diff options
1911 files changed, 56700 insertions, 24268 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..7e800609e6c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG merge=union
\ No newline at end of file diff --git a/.gitignore b/.gitignore index 2c6b65b7b7d..3e30fb8cf77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,42 +1,42 @@ +*.log +*.swp +.DS_Store .bundle +.chef +.directory +.envrc +.gitlab_shell_secret +.idea +.rbenv-version .rbx/ -db/*.sqlite3 -db/*.sqlite3-journal -log/*.log* -tmp/ -.sass-cache/ -coverage/* -backups/* -*.swp -public/uploads/ -.ruby-version .ruby-gemset +.ruby-version .rvmrc -.rbenv-version -.directory -nohup.out -Vagrantfile +.sass-cache/ +.secret .vagrant -config/gitlab.yml +Vagrantfile +backups/* +config/aws.yml config/database.yml +config/gitlab.yml config/initializers/omniauth.rb config/initializers/rack_attack.rb config/initializers/smtp_settings.rb -config/unicorn.rb config/resque.yml -config/aws.yml +config/unicorn.rb +coverage/* +db/*.sqlite3 +db/*.sqlite3-journal db/data.yml -.idea -.DS_Store -.chef -vendor/bundle/* -rails_best_practices_output.html doc/code/* -.secret -*.log -public/uploads.* -public/assets/ -.envrc dump.rdb -tags -.gitlab_shell_secret +log/*.log* +nohup.out +public/assets/ +public/uploads.* +public/uploads/ +rails_best_practices_output.html +/tags +tmp/ +vendor/bundle/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000000..4a9574be053 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,45 @@ +# This file is generated by GitLab CI +before_script: + - ./scripts/prepare_build.sh + - ruby -v + - which ruby + - gem install bundler --no-ri --no-rdoc + - cp config/gitlab.yml.example config/gitlab.yml + - touch log/application.log + - touch log/test.log + - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" + - bundle exec rake db:create RAILS_ENV=test +Rspec: + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec + tags: + - ruby + - mysql + +Spinach: + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach + tags: + - ruby + - mysql + +Jasmine: + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake jasmine:ci + tags: + - ruby + - mysql + +Rubocop: + script: + - bundle exec rubocop + tags: + - ruby + - mysql + +Brakeman: + script: + - bundle exec rake brakeman + tags: + - ruby + - mysql diff --git a/.pkgr.yml b/.pkgr.yml index cf96e7916d8..8fc9fddf8f7 100644 --- a/.pkgr.yml +++ b/.pkgr.yml @@ -1,9 +1,12 @@ user: git group: git +services: + - postgres before_precompile: ./bin/pkgr_before_precompile.sh targets: debian-7: &wheezy build_dependencies: + - libkrb5-dev - libicu-dev - cmake - pkg-config @@ -14,6 +17,7 @@ targets: ubuntu-12.04: *wheezy ubuntu-14.04: build_dependencies: + - libkrb5-dev - libicu-dev - cmake - pkg-config @@ -23,6 +27,7 @@ targets: - git centos-6: build_dependencies: + - krb5-devel - libicu-devel - cmake - pkgconfig diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000000..0cc729d3b08 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,1006 @@ +Style/AccessModifierIndentation: + Description: Check indentation of private/protected visibility modifiers. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-public-private-protected' + Enabled: true + +Style/AccessorMethodName: + Description: Check the naming of accessor methods for get_/set_. + Enabled: false + +Style/Alias: + Description: 'Use alias_method instead of alias.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method' + Enabled: true + +Style/AlignArray: + Description: >- + Align the elements of an array literal if they span more than + one line. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays' + Enabled: true + +Style/AlignHash: + Description: >- + Align the elements of a hash literal if they span more than + one line. + Enabled: true + +Style/AlignParameters: + Description: >- + Align the parameters of a method call if they span more + than one line. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent' + Enabled: false + +Style/AndOr: + Description: 'Use &&/|| instead of and/or.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-and-or-or' + Enabled: false + +Style/ArrayJoin: + Description: 'Use Array#join instead of Array#*.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join' + Enabled: false + +Style/AsciiComments: + Description: 'Use only ascii symbols in comments.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments' + Enabled: true + +Style/AsciiIdentifiers: + Description: 'Use only ascii symbols in identifiers.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers' + Enabled: true + +Style/Attr: + Description: 'Checks for uses of Module#attr.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr' + Enabled: false + +Style/BeginBlock: + Description: 'Avoid the use of BEGIN blocks.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks' + Enabled: true + +Style/BarePercentLiterals: + Description: 'Checks if usage of %() or %Q() matches configuration.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q-shorthand' + Enabled: false + +Style/BlockComments: + Description: 'Do not use block comments.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-block-comments' + Enabled: false + +Style/BlockEndNewline: + Description: 'Put end statement of multiline block on its own line.' + Enabled: true + +Style/Blocks: + Description: >- + Avoid using {...} for multi-line blocks (multiline chaining is + always ugly). + Prefer {...} over do...end for single-line blocks. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' + Enabled: true + +Style/BracesAroundHashParameters: + Description: 'Enforce braces style around hash parameters.' + Enabled: false + +Style/CaseEquality: + Description: 'Avoid explicit use of the case equality operator(===).' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality' + Enabled: false + +Style/CaseIndentation: + Description: 'Indentation of when in a case/when/[else/]end.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-when-to-case' + Enabled: true + +Style/CharacterLiteral: + Description: 'Checks for uses of character literals.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-character-literals' + Enabled: true + +Style/ClassAndModuleCamelCase: + Description: 'Use CamelCase for classes and modules.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#camelcase-classes' + Enabled: true + +Style/ClassAndModuleChildren: + Description: 'Checks style of children classes and modules.' + Enabled: false + +Style/ClassCheck: + Description: 'Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.' + Enabled: false + +Style/ClassMethods: + Description: 'Use self when defining module/class methods.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#def-self-singletons' + Enabled: false + +Style/ClassVars: + Description: 'Avoid the use of class variables.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-class-vars' + Enabled: true + +Style/ColonMethodCall: + Description: 'Do not use :: for method call.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons' + Enabled: false + +Style/CommentAnnotation: + Description: >- + Checks formatting of special comments + (TODO, FIXME, OPTIMIZE, HACK, REVIEW). + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords' + Enabled: false + +Style/CommentIndentation: + Description: 'Indentation of comments.' + Enabled: true + +Style/ConstantName: + Description: 'Constants should use SCREAMING_SNAKE_CASE.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#screaming-snake-case' + Enabled: true + +Style/DefWithParentheses: + Description: 'Use def with parentheses when there are arguments.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' + Enabled: false + +Style/DeprecatedHashMethods: + Description: 'Checks for use of deprecated Hash methods.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-key' + Enabled: false + +Style/Documentation: + Description: 'Document classes and non-namespace modules.' + Enabled: false + +Style/DotPosition: + Description: 'Checks the position of the dot in multi-line method calls.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains' + Enabled: false + +Style/DoubleNegation: + Description: 'Checks for uses of double negation (!!).' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang' + Enabled: false + +Style/EachWithObject: + Description: 'Prefer `each_with_object` over `inject` or `reduce`.' + Enabled: false + +Style/ElseAlignment: + Description: 'Align elses and elsifs correctly.' + Enabled: true + +Style/EmptyElse: + Description: 'Avoid empty else-clauses.' + Enabled: false + +Style/EmptyLineBetweenDefs: + Description: 'Use empty lines between defs.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods' + Enabled: false + +Style/EmptyLines: + Description: "Don't use several empty lines in a row." + Enabled: false + +Style/EmptyLinesAroundAccessModifier: + Description: "Keep blank lines around access modifiers." + Enabled: false + +Style/EmptyLinesAroundBlockBody: + Description: "Keeps track of empty lines around block bodies." + Enabled: false + +Style/EmptyLinesAroundClassBody: + Description: "Keeps track of empty lines around class bodies." + Enabled: false + +Style/EmptyLinesAroundModuleBody: + Description: "Keeps track of empty lines around module bodies." + Enabled: false + +Style/EmptyLinesAroundMethodBody: + Description: "Keeps track of empty lines around method bodies." + Enabled: false + +Style/EmptyLiteral: + Description: 'Prefer literals to Array.new/Hash.new/String.new.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash' + Enabled: false + +Style/EndBlock: + Description: 'Avoid the use of END blocks.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks' + Enabled: false + +Style/EndOfLine: + Description: 'Use Unix-style line endings.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf' + Enabled: false + +Style/EvenOdd: + Description: 'Favor the use of Fixnum#even? && Fixnum#odd?' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' + Enabled: false + +Style/FileName: + Description: 'Use snake_case for source file names.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' + Enabled: false + +Style/FlipFlop: + Description: 'Checks for flip flops' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops' + Enabled: false + +Style/For: + Description: 'Checks use of for or each in multiline loops.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-for-loops' + Enabled: false + +Style/FormatString: + Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf' + Enabled: false + +Style/GlobalVars: + Description: 'Do not introduce global variables.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars' + Enabled: false + +Style/GuardClause: + Description: 'Check for conditionals that can be replaced with guard clauses' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' + Enabled: false + +Style/HashSyntax: + Description: >- + Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax + { :a => 1, :b => 2 }. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-literals' + Enabled: true + +Style/IfUnlessModifier: + Description: >- + Favor modifier if/unless usage when you have a + single-line body. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier' + Enabled: false + +Style/IfWithSemicolon: + Description: 'Do not use if x; .... Use the ternary operator instead.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs' + Enabled: false + +Style/IndentationConsistency: + Description: 'Keep indentation straight.' + Enabled: true + +Style/IndentationWidth: + Description: 'Use 2 spaces for indentation.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' + Enabled: true + +Style/IndentArray: + Description: >- + Checks the indentation of the first element in an array + literal. + Enabled: false + +Style/IndentHash: + Description: 'Checks the indentation of the first key in a hash literal.' + Enabled: false + +Style/InfiniteLoop: + Description: 'Use Kernel#loop for infinite loops.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#infinite-loop' + Enabled: false + +Style/Lambda: + Description: 'Use the new lambda literal syntax for single-line blocks.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#lambda-multi-line' + Enabled: false + +Style/LambdaCall: + Description: 'Use lambda.call(...) instead of lambda.(...).' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc-call' + Enabled: false + +Style/LeadingCommentSpace: + Description: 'Comments should start with a space.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space' + Enabled: false + +Style/LineEndConcatenation: + Description: >- + Use \ instead of + or << to concatenate two string literals at + line end. + Enabled: false + +Style/MethodCallParentheses: + Description: 'Do not use parentheses for method calls with no arguments.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens' + Enabled: false + +Style/MethodDefParentheses: + Description: >- + Checks if the method definitions have or don't have + parentheses. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' + Enabled: false + +Style/MethodName: + Description: 'Use the configured style when naming methods.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars' + Enabled: false + +Style/ModuleFunction: + Description: 'Checks for usage of `extend self` in modules.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#module-function' + Enabled: false + +Style/MultilineBlockChain: + Description: 'Avoid multi-line chains of blocks.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' + Enabled: false + +Style/MultilineBlockLayout: + Description: 'Ensures newlines after multiline block do statements.' + Enabled: true + +Style/MultilineIfThen: + Description: 'Do not use then for multi-line if/unless.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-then' + Enabled: false + +Style/MultilineOperationIndentation: + Description: >- + Checks indentation of binary operations that span more than + one line. + Enabled: false + +Style/MultilineTernaryOperator: + Description: >- + Avoid multi-line ?: (the ternary operator); + use if/unless instead. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-ternary' + Enabled: false + +Style/NegatedIf: + Description: >- + Favor unless over if for negative conditions + (or control flow or). + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives' + Enabled: false + +Style/NegatedWhile: + Description: 'Favor until over while for negative conditions.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#until-for-negatives' + Enabled: false + +Style/NestedTernaryOperator: + Description: 'Use one expression per branch in a ternary operator.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary' + Enabled: true + +Style/Next: + Description: 'Use `next` to skip iteration instead of a condition at the end.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' + Enabled: false + +Style/NilComparison: + Description: 'Prefer x.nil? to x == nil.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' + Enabled: true + +Style/NonNilCheck: + Description: 'Checks for redundant nil checks.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks' + Enabled: true + +Style/Not: + Description: 'Use ! instead of not.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not' + Enabled: true + +Style/NumericLiterals: + Description: >- + Add underscores to large numeric literals to improve their + readability. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics' + Enabled: false + +Style/OneLineConditional: + Description: >- + Favor the ternary operator(?:) over + if/then/else/end constructs. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator' + Enabled: true + +Style/OpMethod: + Description: 'When defining binary operators, name the argument other.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg' + Enabled: false + +Style/ParenthesesAroundCondition: + Description: >- + Don't use parentheses around the condition of an + if/unless/while. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if' + Enabled: true + +Style/PercentLiteralDelimiters: + Description: 'Use `%`-literal delimiters consistently' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces' + Enabled: false + +Style/PercentQLiterals: + Description: 'Checks if uses of %Q/%q match the configured preference.' + Enabled: false + +Style/PerlBackrefs: + Description: 'Avoid Perl-style regex back references.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers' + Enabled: false + +Style/PredicateName: + Description: 'Check the names of predicate methods.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark' + Enabled: false + +Style/Proc: + Description: 'Use proc instead of Proc.new.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc' + Enabled: false + +Style/RaiseArgs: + Description: 'Checks the arguments passed to raise/fail.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#exception-class-messages' + Enabled: false + +Style/RedundantBegin: + Description: "Don't use begin blocks when they are not needed." + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#begin-implicit' + Enabled: false + +Style/RedundantException: + Description: "Checks for an obsolete RuntimeException argument in raise/fail." + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-runtimeerror' + Enabled: false + +Style/RedundantReturn: + Description: "Don't use return where it's not required." + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return' + Enabled: true + +Style/RedundantSelf: + Description: "Don't use self where it's not needed." + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-self-unless-required' + Enabled: false + +Style/RegexpLiteral: + Description: >- + Use %r for regular expressions matching more than + `MaxSlashes` '/' characters. + Use %r only for regular expressions matching more than + `MaxSlashes` '/' character. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r' + Enabled: false + +Style/RescueModifier: + Description: 'Avoid using rescue in its modifier form.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-rescue-modifiers' + Enabled: false + +Style/SelfAssignment: + Description: >- + Checks for places where self-assignment shorthand should have + been used. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#self-assignment' + Enabled: false + +Style/Semicolon: + Description: "Don't use semicolons to terminate expressions." + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon' + Enabled: false + +Style/SignalException: + Description: 'Checks for proper usage of fail and raise.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#fail-method' + Enabled: false + +Style/SingleLineBlockParams: + Description: 'Enforces the names of some block params.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks' + Enabled: false + +Style/SingleLineMethods: + Description: 'Avoid single-line methods.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods' + Enabled: false + +Style/SingleSpaceBeforeFirstArg: + Description: >- + Checks that exactly one space is used between a method name + and the first argument for method calls without parentheses. + Enabled: false + +Style/SpaceAfterColon: + Description: 'Use spaces after colons.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' + Enabled: false + +Style/SpaceAfterComma: + Description: 'Use spaces after commas.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' + Enabled: false + +Style/SpaceAfterControlKeyword: + Description: 'Use spaces after if/elsif/unless/while/until/case/when.' + Enabled: false + +Style/SpaceAfterMethodName: + Description: >- + Do not put a space between a method name and the opening + parenthesis in a method definition. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' + Enabled: false + +Style/SpaceAfterNot: + Description: Tracks redundant space after the ! operator. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-bang' + Enabled: false + +Style/SpaceAfterSemicolon: + Description: 'Use spaces after semicolons.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' + Enabled: false + +Style/SpaceBeforeBlockBraces: + Description: >- + Checks that the left block brace has or doesn't have space + before it. + Enabled: false + +Style/SpaceBeforeComma: + Description: 'No spaces before commas.' + Enabled: false + +Style/SpaceBeforeComment: + Description: >- + Checks for missing space between code and a comment on the + same line. + Enabled: false + +Style/SpaceBeforeSemicolon: + Description: 'No spaces before semicolons.' + Enabled: false + +Style/SpaceInsideBlockBraces: + Description: >- + Checks that block braces have or don't have surrounding space. + For blocks taking parameters, checks that the left brace has + or doesn't have trailing space. + Enabled: false + +Style/SpaceAroundEqualsInParameterDefault: + Description: >- + Checks that the equals signs in parameter default assignments + have or don't have surrounding space depending on + configuration. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-around-equals' + Enabled: false + +Style/SpaceAroundOperators: + Description: 'Use spaces around operators.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' + Enabled: false + +Style/SpaceBeforeModifierKeyword: + Description: 'Put a space before the modifier keyword.' + Enabled: false + +Style/SpaceInsideBrackets: + Description: 'No spaces after [ or before ].' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' + Enabled: false + +Style/SpaceInsideHashLiteralBraces: + Description: "Use spaces inside hash literal braces - or don't." + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' + Enabled: true + +Style/SpaceInsideParens: + Description: 'No spaces after ( or before ).' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' + Enabled: false + +Style/SpaceInsideRangeLiteral: + Description: 'No spaces inside range literals.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-inside-range-literals' + Enabled: false + +Style/SpecialGlobalVars: + Description: 'Avoid Perl-style global variables.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms' + Enabled: false + +Style/StringLiterals: + Description: 'Checks if uses of quotes match the configured preference.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals' + Enabled: false + +Style/StringLiteralsInInterpolation: + Description: >- + Checks if uses of quotes inside expressions in interpolated + strings match the configured preference. + Enabled: false + +Style/SymbolProc: + Description: 'Use symbols as procs instead of blocks when possible.' + Enabled: false + +Style/Tab: + Description: 'No hard tabs.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' + Enabled: true + +Style/TrailingBlankLines: + Description: 'Checks trailing blank lines and final newline.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof' + Enabled: true + +Style/TrailingComma: + Description: 'Checks for trailing comma in parameter lists and literals.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' + Enabled: false + +Style/TrailingWhitespace: + Description: 'Avoid trailing whitespace.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace' + Enabled: false + +Style/TrivialAccessors: + Description: 'Prefer attr_* methods to trivial readers/writers.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family' + Enabled: false + +Style/UnlessElse: + Description: >- + Do not use unless with else. Rewrite these with the positive + case first. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless' + Enabled: false + +Style/UnneededCapitalW: + Description: 'Checks for %W when interpolation is not needed.' + Enabled: false + +Style/UnneededPercentQ: + Description: 'Checks for %q/%Q when single quotes or double quotes would do.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q' + Enabled: false + +Style/UnneededPercentX: + Description: 'Checks for %x when `` would do.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-x' + Enabled: false + +Style/VariableInterpolation: + Description: >- + Don't interpolate global, instance and class variables + directly in strings. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#curlies-interpolate' + Enabled: false + +Style/VariableName: + Description: 'Use the configured style when naming variables.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars' + Enabled: false + +Style/WhenThen: + Description: 'Use when x then ... for one-line cases.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#one-line-cases' + Enabled: false + +Style/WhileUntilDo: + Description: 'Checks for redundant do after while or until.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-while-do' + Enabled: false + +Style/WhileUntilModifier: + Description: >- + Favor modifier while/until usage when you have a + single-line body. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier' + Enabled: false + +Style/WordArray: + Description: 'Use %w or %W for arrays of words.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w' + Enabled: false + +#################### Metrics ################################ + +Metrics/AbcSize: + Description: >- + A calculated magnitude based on number of assignments, + branches, and conditions. + Enabled: false + +Metrics/BlockNesting: + Description: 'Avoid excessive block nesting' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count' + Enabled: false + +Metrics/ClassLength: + Description: 'Avoid classes longer than 100 lines of code.' + Enabled: false + +Metrics/CyclomaticComplexity: + Description: >- + A complexity metric that is strongly correlated to the number + of test cases needed to validate a method. + Enabled: false + +Metrics/LineLength: + Description: 'Limit lines to 80 characters.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits' + Enabled: false + +Metrics/MethodLength: + Description: 'Avoid methods longer than 10 lines of code.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods' + Enabled: false + +Metrics/ParameterLists: + Description: 'Avoid parameter lists longer than three or four parameters.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params' + Enabled: false + +Metrics/PerceivedComplexity: + Description: >- + A complexity metric geared towards measuring complexity for a + human reader. + Enabled: false + +#################### Lint ################################ +### Warnings + +Lint/AmbiguousOperator: + Description: >- + Checks for ambiguous operators in the first argument of a + method invocation without parentheses. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-as-args' + Enabled: false + +Lint/AmbiguousRegexpLiteral: + Description: >- + Checks for ambiguous regexp literals in the first argument of + a method invocation without parenthesis. + Enabled: false + +Lint/AssignmentInCondition: + Description: "Don't use assignment in conditions." + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition' + Enabled: false + +Lint/BlockAlignment: + Description: 'Align block ends correctly.' + Enabled: false + +Lint/ConditionPosition: + Description: >- + Checks for condition placed in a confusing position relative to + the keyword. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition' + Enabled: false + +Lint/Debugger: + Description: 'Check for debugger calls.' + Enabled: false + +Lint/DefEndAlignment: + Description: 'Align ends corresponding to defs correctly.' + Enabled: false + +Lint/DeprecatedClassMethods: + Description: 'Check for deprecated class method calls.' + Enabled: false + +Lint/ElseLayout: + Description: 'Check for odd code arrangement in an else block.' + Enabled: false + +Lint/EmptyEnsure: + Description: 'Checks for empty ensure block.' + Enabled: false + +Lint/EmptyInterpolation: + Description: 'Checks for empty string interpolation.' + Enabled: false + +Lint/EndAlignment: + Description: 'Align ends correctly.' + Enabled: false + +Lint/EndInMethod: + Description: 'END blocks should not be placed inside method definitions.' + Enabled: false + +Lint/EnsureReturn: + Description: 'Do not use return in an ensure block.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-return-ensure' + Enabled: false + +Lint/Eval: + Description: 'The use of eval represents a serious security risk.' + Enabled: false + +Lint/HandleExceptions: + Description: "Don't suppress exception." + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions' + Enabled: false + +Lint/InvalidCharacterLiteral: + Description: >- + Checks for invalid character literals with a non-escaped + whitespace character. + Enabled: false + +Lint/LiteralInCondition: + Description: 'Checks of literals used in conditions.' + Enabled: false + +Lint/LiteralInInterpolation: + Description: 'Checks for literals used in interpolation.' + Enabled: false + +Lint/Loop: + Description: >- + Use Kernel#loop with break rather than begin/end/until or + begin/end/while for post-loop tests. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break' + Enabled: false + +Lint/ParenthesesAsGroupedExpression: + Description: >- + Checks for method calls with a space before the opening + parenthesis. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' + Enabled: true + +Lint/RequireParentheses: + Description: >- + Use parentheses in the method call to avoid confusion + about precedence. + Enabled: false + +Lint/RescueException: + Description: 'Avoid rescuing the Exception class.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues' + Enabled: false + +Lint/ShadowingOuterLocalVariable: + Description: >- + Do not use the same name as outer local variable + for block arguments or block local variables. + Enabled: false + +Lint/SpaceBeforeFirstArg: + Description: >- + Put a space between a method name and the first argument + in a method call without parentheses. + Enabled: false + +Lint/StringConversionInInterpolation: + Description: 'Checks for Object#to_s usage in string interpolation.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s' + Enabled: false + +Lint/UnderscorePrefixedVariableName: + Description: 'Do not use prefix `_` for a variable that is used.' + Enabled: true + +Lint/UnusedBlockArgument: + Description: 'Checks for unused block arguments.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' + Enabled: false + +Lint/UnusedMethodArgument: + Description: 'Checks for unused method arguments.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' + Enabled: false + +Lint/UnreachableCode: + Description: 'Unreachable code.' + Enabled: false + +Lint/UselessAccessModifier: + Description: 'Checks for useless access modifiers.' + Enabled: false + +Lint/UselessAssignment: + Description: 'Checks for useless assignment to a local variable.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' + Enabled: false + +Lint/UselessComparison: + Description: 'Checks for comparison of something with itself.' + Enabled: false + +Lint/UselessElseWithoutRescue: + Description: 'Checks for useless `else` in `begin..end` without `rescue`.' + Enabled: false + +Lint/UselessSetterCall: + Description: 'Checks for useless setter call to a local variable.' + Enabled: false + +Lint/Void: + Description: 'Possible use of operator/literal/variable in void context.' + Enabled: false + +##################### Rails ################################## + +Rails/ActionFilter: + Description: 'Enforces consistent use of action filter methods.' + Enabled: true + +Rails/DefaultScope: + Description: 'Checks if the argument passed to default_scope is a block.' + Enabled: false + +Rails/Delegate: + Description: 'Prefer delegate method for delegations.' + Enabled: false + +Rails/HasAndBelongsToMany: + Description: 'Prefer has_many :through to has_and_belongs_to_many.' + Enabled: true + +Rails/Output: + Description: 'Checks for calls to puts, print, etc.' + Enabled: true + +Rails/ReadWriteAttribute: + Description: >- + Checks for read_attribute(:attr) and + write_attribute(:attr, val). + Enabled: false + +Rails/ScopeArgs: + Description: 'Checks the arguments of ActiveRecord scopes.' + Enabled: false + +Rails/Validation: + Description: 'Use validates :attribute, hash of validations.' + Enabled: false + + +# Exclude some of GitLab files +# +# +AllCops: + RunRailsCops: true + Exclude: + - 'spec/**/*' + - 'features/**/*' + - 'vendor/**/*' + - 'db/**/*' + - 'tmp/**/*' + - 'bin/**/*' + - 'lib/backup/**/*' + - 'lib/tasks/**/*' + - 'lib/email_validator.rb' + - 'lib/gitlab/upgrader.rb' + - 'lib/gitlab/seeder.rb' diff --git a/.ruby-version b/.ruby-version index ac2cdeba013..399088bf465 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.3 +2.1.6 diff --git a/CHANGELOG b/CHANGELOG index ba3cdfa0e52..455a4339420 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,28 +1,538 @@ +Please view this file on the master branch, on stable branches it's out of date. + +v 7.13.0 (unreleased) + - Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu) + - Support commenting on diffs in side-by-side mode (Stan Hu) + - Fix JavaScript error when clicking on the comment button on a diff line that has a comment already (Stan Hu) + - Remove project visibility icons from dashboard projects list + - Rename "Design" profile settings page to "Preferences". + - Allow users to customize their default Dashboard page. + - Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8 + - Convert CRLF newlines to LF when committing using the web editor. + +v 7.12.0 (unreleased) + - Fix Error 500 when one user attempts to access a personal, internal snippet (Stan Hu) + - Disable changing of target branch in new merge request page when a branch has already been specified (Stan Hu) + - Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu) + - Update oauth button logos for Twitter and Google to recommended assets + - Fix hooks for web based events with external issue references (Daniel Gerhardt) + - Update browser gem to version 0.8.0 for IE11 support (Stan Hu) + - Fix timeout when rendering file with thousands of lines. + - Add "Remember me" checkbox to LDAP signin form. + - Add session expiration delay configuration through UI application settings + - Fix external issue tracker hook/test for HTTPS URLs (Daniel Gerhardt) + - Don't notify users mentioned in code blocks or blockquotes. + - Omit link to generate labels if user does not have access to create them (Stan Hu) + - Show warning when a comment will add 10 or more people to the discussion. + - Disable changing of the source branch in merge request update API (Stan Hu) + - Shorten merge request WIP text. + - Add option to disallow users from registering any application to use GitLab as an OAuth provider + - Support editing target branch of merge request (Stan Hu) + - Refactor permission checks with issues and merge requests project settings (Stan Hu) + - Fix Markdown preview not working in Edit Milestone page (Stan Hu) + - Fix Zen Mode not closing with ESC key (Stan Hu) + - Allow HipChat API version to be blank and default to v2 (Stan Hu) + - Add file attachment support in Milestone description (Stan Hu) + - Fix milestone "Browse Issues" button. + - Set milestone on new issue when creating issue from index with milestone filter active. + - Make namespace API available to all users (Stan Hu) + - Add web hook support for note events (Stan Hu) + - Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu) + - Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu) + - Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu) + - Fix git blame syntax highlighting when different commits break up lines (Stan Hu) + - Add "Resend confirmation e-mail" link in profile settings (Stan Hu) + - Allow to configure location of the `.gitlab_shell_secret` file. (Jakub Jirutka) + - Disabled expansion of top/bottom blobs for new file diffs + - Update Asciidoctor gem to version 1.5.2. (Jakub Jirutka) + - Fix resolving of relative links to repository files in AsciiDoc documents. (Jakub Jirutka) + - Use the user list from the target project in a merge request (Stan Hu) + - Default extention for wiki pages is now .md instead of .markdown (Jeroen van Baarsen) + - Add validation to wiki page creation (only [a-zA-Z0-9/_-] are allowed) (Jeroen van Baarsen) + - Fix new/empty milestones showing 100% completion value (Jonah Bishop) + - Add a note when an Issue or Merge Request's title changes + - Consistently refer to MRs as either Accepted or Rejected. + - Add Accepted and Rejected tabs to MR lists. + - Prefix EmailsOnPush email subject with `[Git]`. + - Group project contributions by both name and email. + - Clarify navigation labels for Project Settings and Group Settings. + - Move user avatar and logout button to sidebar + - You can not remove user if he/she is an only owner of group + - User should be able to leave group. If not - show him proper message + - User has ability to leave project + - Add SAML support as an omniauth provider + - Allow to configure a URL to show after sign out + - Add an option to automatically sign-in with an Omniauth provider + - Better performance for web editor (switched from satellites to rugged) + - GitLab CI service sends .gitlab-ci.yml in each push call + - When remove project - move repository and schedule it removal + - Improve group removing logic + - Trigger create-hooks on backup restore task + - Add option to automatically link omniauth and LDAP identities + +v 7.11.4 + - Fix missing bullets when creating lists + - Set rel="nofollow" on external links + +v 7.11.3 + - no changes + - Fix upgrader script (Martins Polakovs) + +v 7.11.2 + - no changes + +v 7.11.1 + - no changes + +v 7.11.0 + - Fall back to Plaintext when Syntaxhighlighting doesn't work. Fixes some buggy lexers (Hannes Rosenögger) + - Get editing comments to work in Chrome 43 again. + - Allow special character in users bio. I.e.: I <3 GitLab + +v 7.11.0 + - Fix broken view when viewing history of a file that includes a path that used to be another file (Stan Hu) + - Don't show duplicate deploy keys + - Fix commit time being displayed in the wrong timezone in some cases (Hannes Rosenögger) + - Make the first branch pushed to an empty repository the default HEAD (Stan Hu) + - Fix broken view when using a tag to display a tree that contains git submodules (Stan Hu) + - Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu) + - Add application setting to restrict user signups to e-mail domains (Stan Hu) + - Don't allow a merge request to be merged when its title starts with "WIP". + - Add a page title to every page. + - Allow primary email to be set to an email that you've already added. + - Fix clone URL field and X11 Primary selection (Dmitry Medvinsky) + - Ignore invalid lines in .gitmodules + - Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu) + - Redirect to sign in page after signing out. + - Fix "Hello @username." references not working by no longer allowing usernames to end in period. + - Fix "Revspec not found" errors when viewing diffs in a forked project with submodules (Stan Hu) + - Improve project page UI + - Fix broken file browsing with relative submodule in personal projects (Stan Hu) + - Add "Reply quoting selected text" shortcut key (`r`) + - Fix bug causing `@whatever` inside an issue's first code block to be picked up as a user mention. + - Fix bug causing `@whatever` inside an inline code snippet (backtick-style) to be picked up as a user mention. + - When use change branches link at MR form - save source branch selection instead of target one + - Improve handling of large diffs + - Added GitLab Event header for project hooks + - Add Two-factor authentication (2FA) for GitLab logins + - Show Atom feed buttons everywhere where applicable. + - Add project activity atom feed. + - Don't crash when an MR from a fork has a cross-reference comment from the target project on one of its commits. + - Explain how to get a new password reset token in welcome emails + - Include commit comments in MR from a forked project. + - Group milestones by title in the dashboard and all other issue views. + - Query issues, merge requests and milestones with their IID through API (Julien Bianchi) + - Add default project and snippet visibility settings to the admin web UI. + - Show incompatible projects in Google Code import status (Stan Hu) + - Fix bug where commit data would not appear in some subdirectories (Stan Hu) + - Task lists are now usable in comments, and will show up in Markdown previews. + - Fix bug where avatar filenames were not actually deleted from the database during removal (Stan Hu) + - Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu) + - Protect OmniAuth request phase against CSRF. + - Don't send notifications to mentioned users that don't have access to the project in question. + - Add search issues/MR by number + - Move snippets UI to fluid layout + - Improve UI for sidebar. Increase separation between navigation and content + - Improve new project command options (Ben Bodenmiller) + - Add common method to force UTF-8 and use it to properly handle non-ascii OAuth user properties (Onur Küçük) + - Prevent sending empty messages to HipChat (Chulki Lee) + - Improve UI for mobile phones on dashboard and project pages + - Add room notification and message color option for HipChat + - Allow to use non-ASCII letters and dashes in project and namespace name. (Jakub Jirutka) + - Add footnotes support to Markdown (Guillaume Delbergue) + - Add current_sign_in_at to UserFull REST api. + - Make Sidekiq MemoryKiller shutdown signal configurable + - Add "Create Merge Request" buttons to commits and branches pages and push event. + - Show user roles by comments. + - Fix automatic blocking of auto-created users from Active Directory. + - Call merge request web hook for each new commits (Arthur Gautier) + - Use SIGKILL by default in Sidekiq::MemoryKiller + - Fix mentioning of private groups. + - Add style for <kbd> element in markdown + - Spin spinner icon next to "Checking for CI status..." on MR page. + - Fix reference links in dashboard activity and ATOM feeds. + - Ensure that the first added admin performs repository imports + +v 7.10.4 + - Fix migrations broken in 7.10.2 + - Make tags for GitLab installations running on MySQL case sensitive + - Get Gitorious importer to work again. + - Fix adding new group members from admin area + - Fix DB error when trying to tag a repository (Stan Hu) + - Fix Error 500 when searching Wiki pages (Stan Hu) + - Unescape branch names in compare commit (Stan Hu) + - Order commit comments chronologically in API. + +v 7.10.2 + - Fix CI links on MR page + +v 7.10.0 + - Ignore submodules that are defined in .gitmodules but are checked in as directories. + - Allow projects to be imported from Google Code. + - Remove access control for uploaded images to fix broken images in emails (Hannes Rosenögger) + - Allow users to be invited by email to join a group or project. + - Don't crash when project repository doesn't exist. + - Add config var to block auto-created LDAP users. + - Don't use HTML ellipsis in EmailsOnPush subject truncated commit message. + - Set EmailsOnPush reply-to address to committer email when enabled. + - Fix broken file browsing with a submodule that contains a relative link (Stan Hu) + - Fix persistent XSS vulnerability around profile website URLs. + - Fix project import URL regex to prevent arbitary local repos from being imported. + - Fix directory traversal vulnerability around uploads routes. + - Fix directory traversal vulnerability around help pages. + - Don't leak existence of project via search autocomplete. + - Don't leak existence of group or project via search. + - Fix bug where Wiki pages that included a '/' were no longer accessible (Stan Hu) + - Fix bug where error messages from Dropzone would not be displayed on the issues page (Stan Hu) + - Add a rake task to check repository integrity with `git fsck` + - Add ability to configure Reply-To address in gitlab.yml (Stan Hu) + - Move current user to the top of the list in assignee/author filters (Stan Hu) + - Fix broken side-by-side diff view on merge request page (Stan Hu) + - Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu) + - Allow HTML tags in Markdown input + - Fix code unfold not working on Compare commits page (Stan Hu) + - Fix generating SSH key fingerprints with OpenSSH 6.8. (Sašo Stanovnik) + - Fix "Import projects from" button to show the correct instructions (Stan Hu) + - Fix dots in Wiki slugs causing errors (Stan Hu) + - Make maximum attachment size configurable via Application Settings (Stan Hu) + - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) + - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu) + - Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu) + - Reduce Rack Attack false positives causing 403 errors during HTTP authentication (Stan Hu) + - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) + - Fix a link in the patch update guide + - Add a service to support external wikis (Hannes Rosenögger) + - Omit the "email patches" link and fix plain diff view for merge commits + - List new commits for newly pushed branch in activity view. + - Add sidetiq gem dependency to match EE + - Add changelog, license and contribution guide links to project tab bar. + - Improve diff UI + - Fix alignment of navbar toggle button (Cody Mize) + - Fix checkbox rendering for nested task lists + - Identical look of selectboxes in UI + - Upgrade the gitlab_git gem to version 7.1.3 + - Move "Import existing repository by URL" option to button. + - Improve error message when save profile has error. + - Passing the name of pushed ref to CI service (requires GitLab CI 7.9+) + - Add location field to user profile + - Fix print view for markdown files and wiki pages + - Fix errors when deleting old backups + - Improve GitLab performance when working with git repositories + - Add tag message and last commit to tag hook (Kamil Trzciński) + - Restrict permissions on backup files + - Improve oauth accounts UI in profile page + - Add ability to unlink connected accounts + - Replace commits calendar with faster contribution calendar that includes issues and merge requests + - Add inifinite scroll to user page activity + - Don't include system notes in issue/MR comment count. + - Don't mark merge request as updated when merge status relative to target branch changes. + - Link note avatar to user. + - Make Git-over-SSH errors more descriptive. + - Fix EmailsOnPush. + - Refactor issue filtering + - AJAX selectbox for issue assignee and author filters + - Fix issue with missing options in issue filtering dropdown if selected one + - Prevent holding Control-Enter or Command-Enter from posting comment multiple times. + - Prevent note form from being cleared when submitting failed. + - Improve file icons rendering on tree (Sullivan Sénéchal) + - API: Add pagination to project events + - Get issue links in notification mail to work again. + - Don't show commit comment button when user is not signed in. + - Fix admin user projects lists. + - Don't leak private group existence by redirecting from namespace controller to group controller. + - Ability to skip some items from backup (database, respositories or uploads) + - Archive repositories in background worker. + - Import GitHub, Bitbucket or GitLab.com projects owned by authenticated user into current namespace. + - Project labels are now available over the API under the "tag_list" field (Cristian Medina) + - Fixed link paths for HTTP and SSH on the admin project view (Jeremy Maziarz) + - Fix and improve help rendering (Sullivan Sénéchal) + - Fix final line in EmailsOnPush email diff being rendered as error. + - Prevent duplicate Buildkite service creation. + - Fix git over ssh errors 'fatal: protocol error: bad line length character' + - Automatically setup GitLab CI project for forks if origin project has GitLab CI enabled + - Bust group page project list cache when namespace name or path changes. + - Explicitly set image alt-attribute to prevent graphical glitches if gravatars could not be loaded + - Allow user to choose a public email to show on public profile + - Remove truncation from issue titles on milestone page (Jason Blanchard) + - 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.3 + - Contains no changes + +v 7.9.2 + - Contains no changes + +v 7.9.1 + - Include missing events and fix save functionality in admin service template settings form (Stan Hu) + - Fix "Import projects from" button to show the correct instructions (Stan Hu) + - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu) + - Fix for LDAP with commas in DN + - Fix missing events and in admin Slack service template settings form (Stan Hu) + - Don't show commit comment button when user is not signed in. + - Downgrade gemnasium-gitlab-service gem + +v 7.9.0 + - Add HipChat integration documentation (Stan Hu) + - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu) + - Fix broken email images (Hannes Rosenögger) + - Automatically config git if user forgot, where possible (Zeger-Jan van de Weg) + - Fix mass SQL statements on initial push (Hannes Rosenögger) + - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu) + - Add comment notification events to HipChat and Slack services (Stan Hu) + - Add issue and merge request events to HipChat and Slack services (Stan Hu) + - Fix merge request URL passed to Webhooks. (Stan Hu) + - Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu) + - Fix code preview theme setting for comments, issues, merge requests, and snippets (Stan Hu) + - Move labels/milestones tabs to sidebar + - Upgrade Rails gem to version 4.1.9. + - Improve error messages for file edit failures + - Improve UI for commits, issues and merge request lists + - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. + - Allow admins to override restricted project visibility settings. + - Move restricted visibility settings from gitlab.yml into the web UI. + - Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev) + - Save web edit in new branch + - Fix ordering of imported but unchanged projects (Marco Wessel) + - Mobile UI improvements: make aside content expandable + - Expose avatar_url in projects API + - Fix checkbox alignment on the application settings page. + - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger) + - Fix mass-unassignment of issues (Robert Speicher) + - Fix hidden diff comments in merge request discussion view + - Allow user confirmation to be skipped for new users via API + - Add a service to send updates to an Irker gateway (Romain Coltel) + - Add brakeman (security scanner for Ruby on Rails) + - Slack username and channel options + - Add grouped milestones from all projects to dashboard. + - Web hook sends pusher email as well as commiter + - Add Bitbucket omniauth provider. + - Add Bitbucket importer. + - Support referencing issues to a project whose name starts with a digit + - Condense commits already in target branch when updating merge request source branch. + - Send notifications and leave system comments when bulk updating issues. + - Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison) + - Move groups page from profile to dashboard + - Starred projects page at dashboard + - Blocking user does not remove him/her from project/groups but show blocked label + - Change subject of EmailsOnPush emails to include namespace, project and branch. + - Change subject of EmailsOnPush emails to include first commit message when multiple were pushed. + - Remove confusing footer from EmailsOnPush mail body. + - Add list of changed files to EmailsOnPush emails. + - Add option to send EmailsOnPush emails from committer email if domain matches. + - Add option to disable code diffs in EmailOnPush emails. + - Wrap commit message in EmailsOnPush email. + - Send EmailsOnPush emails when deleting commits using force push. + - Fix EmailsOnPush email comparison link to include first commit. + - Fix highliht of selected lines in file + - Reject access to group/project avatar if the user doesn't have access. + - Add database migration to clean group duplicates with same path and name (Make sure you have a backup before update) + - Add GitLab active users count to rake gitlab:check + - Starred projects page at dashboard + - Make email display name configurable + - Improve json validation in hook data + - Use Emoji One + - Updated emoji help documentation to properly reference EmojiOne. + - Fix missing GitHub organisation repositories on import page. + - Added blue theme + - Remove annoying notice messages when create/update merge request + - Allow smb:// links in Markdown text. + - Filter merge request by title or description at Merge Requests page + - Block user if he/she was blocked in Active Directory + - Fix import pages not working after first load. + - Use custom LDAP label in LDAP signin form. + - Execute hooks and services when branch or tag is created or deleted through web interface. + - Block and unblock user if he/she was blocked/unblocked in Active Directory + - Raise recommended number of unicorn workers from 2 to 3 + - Use same layout and interactivity for project members as group members. + - Prevent gitlab-shell character encoding issues by receiving its changes as raw data. + - Ability to unsubscribe/subscribe to issue or merge request + - Delete deploy key when last connection to a project is destroyed. + - Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther) + - Backup of repositories with tar instead of git bundle (only now are git-annex files included in the backup) + - Add canceled status for CI + - Send EmailsOnPush email when branch or tag is created or deleted. + - Faster merge request processing for large repository + - Prevent doubling AJAX request with each commit visit via Turbolink + - Prevent unnecessary doubling of js events on import pages and user calendar + +v 7.8.4 + - Fix issue_tracker_id substitution in custom issue trackers + - Fix path and name duplication in namespaces + +v 7.8.3 + - Bump version of gitlab_git fixing annotated tags without message + +v 7.8.2 + - Fix service migration issue when upgrading from versions prior to 7.3 + - Fix setting of the default use project limit via admin UI + - Fix showing of already imported projects for GitLab and Gitorious importers + - Fix response of push to repository to return "Not found" if user doesn't have access + - Fix check if user is allowed to view the file attachment + - Fix import check for case sensetive namespaces + - Increase timeout for Git-over-HTTP requests to 1 hour since large pulls/pushes can take a long time. + - Properly handle autosave local storage exceptions. + - Escape wildcards when searching LDAP by username. + +v 7.8.1 + - Fix run of custom post receive hooks + - Fix migration that caused issues when upgrading to version 7.8 from versions prior to 7.3 + - Fix the warning for LDAP users about need to set password + - Fix avatars which were not shown for non logged in users + - Fix urls for the issues when relative url was enabled + +v 7.8.0 + - Fix access control and protection against XSS for note attachments and other uploads. + - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) + - Make project search case insensitive (Hannes Rosenögger) + - Include issue/mr participants in list of recipients for reassign/close/reopen emails + - Expose description in groups API + - Better UI for project services page + - Cleaner UI for web editor + - Add diff syntax highlighting in email-on-push service notifications (Hannes Rosenögger) + - Add API endpoint to fetch all changes on a MergeRequest (Jeroen van Baarsen) + - View note image attachments in new tab when clicked instead of downloading them + - Improve sorting logic in UI and API. Explicitly define what sorting method is used by default + - Fix overflow at sidebar when have several items + - Add notes for label changes in issue and merge requests + - Show tags in commit view (Hannes Rosenögger) + - Only count a user's vote once on a merge request or issue (Michael Clarke) + - Increase font size when browse source files and diffs + - Service Templates now let you set default values for all services + - Create new file in empty repository using GitLab UI + - Ability to clone project using oauth2 token + - Upgrade Sidekiq gem to version 3.3.0 + - Stop git zombie creation during force push check + - Show success/error messages for test setting button in services + - Added Rubocop for code style checks + - Fix commits pagination + - Async load a branch information at the commit page + - Disable blacklist validation for project names + - Allow configuring protection of the default branch upon first push (Marco Wessel) + - Add gitlab.com importer + - Add an ability to login with gitlab.com + - Add a commit calendar to the user profile (Hannes Rosenögger) + - Submit comment on command-enter + - Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`. + - Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" (Julien Bianchi and Hannes Rosenögger) + - Fix long broadcast message cut-off on left sidebar (Visay Keo) + - Add Project Avatars (Steven Thonus and Hannes Rosenögger) + - Password reset token validity increased from 2 hours to 2 days since it is also send on account creation. + - Edit group members via API + - Enable raw image paste from clipboard, currently Chrome only (Marco Cyriacks) + - Add action property to merge request hook (Julien Bianchi) + - Remove duplicates from group milestone participants list. + - Add a new API function that retrieves all issues assigned to a single milestone (Justin Whear and Hannes Rosenögger) + - API: Access groups with their path (Julien Bianchi) + - Added link to milestone and keeping resource context on smaller viewports for issues and merge requests (Jason Blanchard) + - Allow notification email to be set separately from primary email. + - API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger) + - Don't have Markdown preview fail for long comments/wiki pages. + - When test web hook - show error message instead of 500 error page if connection to hook url was reset + - Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov) + - Added persistent collapse button for left side nav bar (Jason Blanchard) + - Prevent losing unsaved comments by automatically restoring them when comment page is loaded again. + - Don't allow page to be scaled on mobile. + - Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up. + - Show assignees in merge request index page (Kelvin Mutuma) + - Link head panel titles to relevant root page. + - Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S). + - Show users button to share their newly created public or internal projects on twitter + - Add quick help links to the GitLab pricing and feature comparison pages. + - Fix duplicate authorized applications in user profile and incorrect application client count in admin area. + - Make sure Markdown previews always use the same styling as the eventual destination. + - Remove deprecated Group#owner_id from API + - Show projects user contributed to on user page. Show stars near project on user page. + - Improve database performance for GitLab + - Add Asana service (Jeremy Benoist) + - Improve project web hooks with extra data + +v 7.7.2 + - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch + - Fix issue when LDAP user can't login with existing GitLab account + +v 7.7.1 + - Improve mention autocomplete performance + - Show setup instructions for GitHub import if disabled + - Allow use http for OAuth applications + +v 7.7.0 + - Import from GitHub.com feature + - Add Jetbrains Teamcity CI service (Jason Lippert) + - Mention notification level + - Markdown preview in wiki (Yuriy Glukhov) + - Raise group avatar filesize limit to 200kb + - OAuth applications feature + - Show user SSH keys in admin area + - Developer can push to protected branches option + - Set project path instead of project name in create form + - Block Git HTTP access after 10 failed authentication attempts + - Updates to the messages returned by API (sponsored by O'Reilly Media) + - New UI layout with side navigation + - Add alert message in case of outdated browser (IE < 10) + - Added API support for sorting projects + - Update gitlab_git to version 7.0.0.rc14 + - Add API project search filter option for authorized projects + - Fix File blame not respecting branch selection + - Change some of application settings on fly in admin area UI + - Redesign signin/signup pages + - Close standard input in Gitlab::Popen.popen + - Trigger GitLab CI when push tags + - When accept merge request - do merge using sidaekiq job + - Enable web signups by default + - Fixes for diff comments: drag-n-drop images, selecting images + - Fixes for edit comments: drag-n-drop images, preview mode, selecting images, save & update + - Remove password strength indicator + + + v 7.6.0 - Fork repository to groups - New rugged version - Add CRON=1 backup setting for quiet backups - Fix failing wiki restore - - - Add optional Sidekiq MemoryKiller middleware (enabled via SIDEKIQ_MAX_RSS env variable) - - - - + - Monokai highlighting style now more faithful to original design (Mark Riedesel) - Create project with repository in synchrony - Added ability to create empty repo or import existing one if project does not have repository - - - - - Reactivate highlight.js language autodetection - Mobile UI improvements - - - Change maximum avatar file size from 100KB to 200KB - - - - - - - - - - + - Strict validation for snippet file names + - Enable Markdown preview for issues, merge requests, milestones, and notes (Vinnie Okada) + - In the docker directory is a container template based on the Omnibus packages. + - Update Sidekiq to version 2.17.8 + - Add author filter to project issues and merge requests pages + - Atom feed for user activity + - Support multiple omniauth providers for the same user + - Rendering cross reference in issue title and tooltip for merge request + - Show username in comments + - Possibility to create Milestones or Labels when Issues are disabled + - Fix bug with showing gpg signature in tag + +v 7.5.3 + - Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2) v 7.5.2 - Don't log Sidekiq arguments by default + - Fix restore of wiki repositories from backups + +v 7.5.1 + - Add missing timestamps to 'members' table v 7.5.0 - API: Add support for Hipchat (Kevin Houdebert) @@ -42,7 +552,7 @@ v 7.5.0 - Performance improvements - Fix post-receive issue for projects with deleted forks - New gitlab-shell version with custom hooks support - - Improve code + - Improve code - GitLab CI 5.2+ support (does not support older versions) - Fixed bug when you can not push commits starting with 000000 to protected branches - Added a password strength indicator @@ -52,6 +562,12 @@ v 7.5.0 - Use secret token with GitLab internal API. - Add missing timestamps to 'members' table +v 7.4.5 + - Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2) + +v 7.4.4 + - No changes + v 7.4.3 - Fix raw snippets view - Fix security issue for member api diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9da89cc2107..a9dcf67b1e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,13 +20,18 @@ Please treat our volunteers with courtesy and respect, it will go a long way tow Issues and merge requests should be in English and contain appropriate language for audiences of all ages. -## Issue tracker +## Helping others + +Please help other GitLab users when you can. +The channnels people will reach out on can be found on the [getting help page](https://about.gitlab.com/getting-help/). +Sign up for the mailinglist, answer GitLab questions on StackOverflow or respond in the irc channel. +You can also sign up on [CodeTriage](http://www.codetriage.com/gitlabhq/gitlabhq) to help with one issue every day. -To get support for your particular problem please use the channels as detailed in the [getting help section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#getting-help). Professional [support subscriptions](http://about.gitlab.com/subscription/) and [consulting services](http://about.gitlab.com/consultancy/) are available from [GitLab.com](http://about.gitlab.com/). +## Issue tracker -The [issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) is only for obvious errors in the latest [stable or development release of GitLab](MAINTENANCE.md). If something is wrong but it is not a regression compared to older versions of GitLab please do not open an issue but a feature request. When submitting an issue please conform to the issue submission guidelines listed below. Not all issues will be addressed and your issue is more likely to be addressed if you submit a merge request which partially or fully addresses the issue. +To get support for your particular problem please use the [getting help channels](https://about.gitlab.com/getting-help/). -Issues can be filed either at [gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues) or [github.com](https://github.com/gitlabhq/gitlabhq/issues). +The [GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues) is only for obvious errors in the latest [stable or development release of GitLab](MAINTENANCE.md). If something is wrong but it is not a regression compared to older versions of GitLab please do not open an issue but a feature request. When submitting an issue please conform to the issue submission guidelines listed below. Not all issues will be addressed and your issue is more likely to be addressed if you submit a merge request which partially or fully addresses the issue. Do not use the issue tracker for feature requests. We have a specific [feature request forum](http://feedback.gitlab.com) for this purpose. Please keep feature requests as small and simple as possible, complex ones might be edited to make them small and simple. @@ -37,7 +42,7 @@ Please send a merge request with a tested solution or a merge request with a fai **[Search the issues](https://gitlab.com/gitlab-org/gitlab-ce/issues)** for similar entries before submitting your own, there's a good chance somebody else had the same issue. Show your support with `:+1:` and/or join the discussion. Please submit issues in the following format (as the first post): 1. **Summary:** Summarize your issue in one sentence (what goes wrong, what did you expect to happen) -1. **Steps to reproduce:** How can we reproduce the issue, preferably on the [GitLab development virtual machine with vagrant](https://gitlab.com/gitlab-org/cookbook-gitlab/blob/master/doc/development.md) (start your issue with: `vagrant destroy && vagrant up && vagrant ssh`) +1. **Steps to reproduce:** How can we reproduce the issue 1. **Expected behavior:** Describe your issue in detail 1. **Observed behavior** 1. **Relevant logs and/or screenshots:** Please use code blocks (\`\`\`) to format console output, logs, and code as it's very hard to read otherwise. @@ -56,14 +61,16 @@ Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org If you are new to GitLab development (or web development in general), search for the label `easyfix` ([gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [github](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint. +To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file. + ### Merge request guidelines If you can, please submit a merge request with the fix or improvements including tests. If you don't know how to fix the issue but can write a test that exposes the issue we will accept that as well. In general bug fixes that include a regression test are merged quickly while new features without proper tests are least likely to receive timely feedback. The workflow to make a merge request is as follows: 1. Fork the project on GitLab Cloud 1. Create a feature branch -1. Write [tests](README.md#run-the-tests) and code -1. Add your changes to the [CHANGELOG](CHANGELOG) insert your line at a [random point](doc/workflow/gitlab_flow.md#do-not-order-commits-with-rebase) in the current version +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 1. If you have multiple commits please combine them into one commit by [squashing them](http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) 1. Push the commit to your fork @@ -75,8 +82,11 @@ If you can, please submit a merge request with the fix or improvements including 1. Link relevant [issues](https://gitlab.com/gitlab-org/gitlab-ce/issues) and/or [feature requests](http://feedback.gitlab.com/) from the merge request description and leave a comment on them with a link back to the MR 1. Be prepared to answer questions and incorporate feedback even if requests for this arrive weeks or months after your MR submission 1. If your MR touches code that executes shell commands, make sure it adheres to the [shell command guidelines]( doc/development/shell_commands.md). +1. Also have a look at the [shell command guidelines](doc/development/shell_commands.md) if your code reads or opens files, or handles paths to files on disk. -The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast. Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as assisting subscribers with upgrade issues, the release of Enterprise Edition and the upgrade of GitLab Cloud. After the 7th it is already getting closer to the release date of the next version. This means there is less time to fix the issues created by merging large new features. +The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast. +Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as regressions requiring patch releases. +After the 7th it is already getting closer to the release date of the next version. This means there is less time to fix the issues created by merging large new features. Please keep the change in a single MR **as small as possible**. If you want to contribute a large feature think very hard what the minimum viable change is. Can you split functionality? Can you only submit the backend/API code? Can you start with a very simple UI? Can you do part of the refactor? The increased reviewability of small MR's that leads to higher code quality is more important to us than having a minimal commit log. The smaller a MR is the more likely it is it will be merged (quickly), after that you can send more MR's to enhance it. @@ -100,6 +110,16 @@ Please ensure you support the feature you contribute through all of these steps. 1. Community questions answered 1. Answers to questions radiated (in docs/wiki/etc.) +If you add a dependency in GitLab (such as an operating system package) please consider updating the following and note the applicability of each in your merge request: + +1. Note the addition in the release blog post (create one if it doesn't exist yet) https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/ +1. Upgrade guide, for example https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/7.5-to-7.6.md +1. Upgrader https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/upgrader.md#2-run-gitlab-upgrade-tool +1. Installation guide https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies +1. GitLab Development Kit https://gitlab.com/gitlab-org/gitlab-development-kit +1. Test suite https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/examples/configure_a_runner_to_run_the_gitlab_ce_test_suite.md +1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab + ## Merge request description format 1. What does this MR do? @@ -117,6 +137,7 @@ Please ensure you support the feature you contribute through all of these steps. 1. Can merge without problems (if not please merge `master`, never rebase commits pushed to the remote server) 1. Does not break any existing functionality 1. Fixes one specific issue or implements one specific feature (do not combine things, send separate merge requests if needed) +1. Migrations should do only one thing (eg: either create a table, move data to a new table or remove an old table) to aid retrying on failure 1. Keeps the GitLab code base clean and well structured 1. Contains functionality we think other users will benefit from too 1. Doesn't add configuration options since they complicate future changes @@ -139,5 +160,24 @@ Please ensure you support the feature you contribute through all of these steps. 1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style#coffeescript) 1. [Shell commands](doc/development/shell_commands.md) created by GitLab contributors to enhance security 1. [Markdown](http://www.cirosantilli.com/markdown-styleguide) +1. [Database Migrations](doc/development/migration_style_guide.md) +1. [Documentation styleguide](doc_styleguide.md) +1. Interface text should be written subjectively instead of objectively. It should be the gitlab core team addressing a person. It should be written in present time and never use past tense (has been/was). For example instead of "prohibited this user from being saved due to the following errors:" the text should be "sorry, we could not create your account because:". Also these [excellent writing guidelines](https://github.com/NARKOZ/guides#writing). This is also the style used by linting tools such as [RuboCop](https://github.com/bbatsov/rubocop), [PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com). + +## Code of conduct + +As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. + +Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. + +This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com + +This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 197c4d5c2d7..ec1cf33c3f6 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.4.0 +2.6.3 @@ -1,17 +1,6 @@ source "https://rubygems.org" -def darwin_only(require_as) - RUBY_PLATFORM.include?('darwin') && require_as -end - -def linux_only(require_as) - RUBY_PLATFORM.include?('linux') && require_as -end - -gem "rails", "~> 4.1.0" - -# Make links from text -gem 'rails_autolink', '~> 1.1' +gem 'rails', '4.1.11' # Default values for AR models gem "default_value_for", "~> 3.0.0" @@ -23,27 +12,49 @@ gem "pg", group: :postgres # Auth gem "devise", '3.2.4' gem "devise-async", '0.9.0' -gem 'omniauth', "~> 1.1.3" +gem 'omniauth', "~> 1.2.2" gem 'omniauth-google-oauth2' gem 'omniauth-twitter' gem 'omniauth-github' gem 'omniauth-shibboleth' +gem 'omniauth-kerberos', group: :kerberos +gem 'omniauth-gitlab' +gem 'omniauth-bitbucket' +gem 'omniauth-saml' +gem 'doorkeeper', '2.1.3' +gem "rack-oauth2", "~> 1.0.5" + +# Two-factor authentication +gem 'devise-two-factor' +gem 'rqrcode-rails3' +gem 'attr_encrypted', '1.3.4' + +# Browser detection +gem "browser", '~> 0.8.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '7.0.0.rc12' +gem "gitlab_git", '~> 7.2.5' # Ruby/Rack Git Smart-HTTP Server Handler -gem 'gitlab-grack', '~> 2.0.0.pre', require: 'grack' +# GitLab fork with a lot of changes (improved thread-safety, better memory usage etc) +# For full list of changes see https://github.com/SaitoWu/grack/compare/master...gitlabhq:master +gem 'gitlab-grack', '~> 2.0.2', require: 'grack' # LDAP Auth -gem 'gitlab_omniauth-ldap', '1.2.0', require: "omniauth-ldap" +# GitLab fork with several improvements to original library. For full list of changes +# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master +gem 'gitlab_omniauth-ldap', '1.2.1', require: "omniauth-ldap" # Git Wiki -gem 'gollum-lib', '~> 3.0.0' +gem 'gollum-lib', '~> 4.0.2' # Language detection -gem "gitlab-linguist", "~> 3.0.0", require: "linguist" +# GitLab fork of linguist does not require pygments/python dependency. +# New version of original gem also dropped pygments support but it has strict +# dependency to unstable rugged version. We have internal issue for replacing +# fork with original gem when we meet on same rugged version - https://dev.gitlab.org/gitlab/gitlabhq/issues/2052. +gem "gitlab-linguist", "~> 3.0.1", require: "linguist" # API gem "grape", "~> 0.6.1" @@ -70,7 +81,7 @@ gem "carrierwave" gem 'dropzonejs-rails' # for aws storage -gem "fog", "~> 1.14" +gem "fog", "~> 1.25.0" gem "unf" # Authorization @@ -79,20 +90,17 @@ gem "six" # Seed data gem "seed-fu" -# Markup pipeline for GitLab -gem 'html-pipeline-gitlab', '~> 0.1.0' - -# Markdown to HTML -gem "github-markup" - -# Required markup gems by github-markdown -gem 'redcarpet', '~> 3.1.2' +# Markdown and HTML processing +gem 'html-pipeline', '~> 1.11.0' +gem 'task_list', '1.0.2', require: 'task_list/railtie' +gem 'github-markup' +gem 'redcarpet', '~> 3.3.0' gem 'RedCloth' -gem 'rdoc', '~>3.6' -gem 'org-ruby', '= 0.9.9' -gem 'creole', '~>0.3.6' -gem 'wikicloth', '=0.8.1' -gem 'asciidoctor', '= 0.1.4' +gem 'rdoc', '~>3.6' +gem 'org-ruby', '= 0.9.12' +gem 'creole', '~>0.3.6' +gem 'wikicloth', '=0.8.1' +gem 'asciidoctor', '~> 1.5.2' # Diffs gem 'diffy', '~> 3.0.3' @@ -107,12 +115,13 @@ end gem "state_machine" # Issue tags -gem "acts-as-taggable-on" +gem 'acts-as-taggable-on', '~> 3.4' # Background jobs gem 'slim' gem 'sinatra', require: nil -gem 'sidekiq', '2.17.0' +gem 'sidekiq', '~> 3.3' +gem 'sidetiq', '0.6.3' # HTTP requests gem "httparty" @@ -134,7 +143,7 @@ gem "redis-rails" gem 'tinder', '~> 1.9.2' # HipChat integration -gem "hipchat", "~> 1.4.0" +gem 'hipchat', '~> 1.5.0' # Flowdock integration gem "gitlab-flowdock-git-hook", "~> 0.4.2" @@ -145,8 +154,14 @@ gem "gemnasium-gitlab-service", "~> 0.2" # Slack integration gem "slack-notifier", "~> 1.0.0" +# Asana integration +gem 'asana', '~> 0.0.6' + # d3 -gem "d3_rails", "~> 3.1.4" +gem 'd3_rails', '~> 3.5.5' + +#cal-heatmap +gem "cal-heatmap-rails", "~> 0.0.1" # underscore-rails gem "underscore-rails", "~> 1.4.4" @@ -155,7 +170,7 @@ gem "underscore-rails", "~> 1.4.4" gem "sanitize", '~> 2.0' # Protect against bruteforcing -gem "rack-attack" +gem "rack-attack", '~> 4.3.0' # Ace editor gem 'ace-rails-ap' @@ -163,43 +178,42 @@ gem 'ace-rails-ap' # Keyboard shortcuts gem 'mousetrap-rails' -# Semantic UI Sass for Sidebar -gem 'semantic-ui-sass', '~> 0.16.1.0' +# Detect and convert string character encoding +gem 'charlock_holmes' gem "sass-rails", '~> 4.0.2' gem "coffee-rails" gem "uglifier" -gem "therubyracer" -gem 'turbolinks' +gem 'turbolinks', '~> 2.5.0' gem 'jquery-turbolinks' -gem 'select2-rails' -gem 'jquery-atwho-rails', "~> 0.3.3" -gem "jquery-rails" -gem "jquery-ui-rails" -gem "jquery-scrollto-rails" -gem "raphael-rails", "~> 2.1.2" -gem 'bootstrap-sass', '~> 3.0' -gem "font-awesome-rails", '~> 4.2' -gem "gitlab_emoji", "~> 0.0.1.1" -gem "gon", '~> 5.0.0' +gem 'addressable' +gem 'bootstrap-sass', '~> 3.0' +gem 'font-awesome-rails', '~> 4.2' +gem 'gitlab_emoji', '~> 0.1' +gem 'gon', '~> 5.0.0' +gem 'jquery-atwho-rails', '~> 1.0.0' +gem 'jquery-rails', '3.1.3' +gem 'jquery-scrollto-rails' +gem 'jquery-ui-rails' gem 'nprogress-rails' +gem 'raphael-rails', '~> 2.1.2' gem 'request_store' -gem "virtus" -gem 'addressable' +gem 'select2-rails' +gem 'virtus' group :development do + gem 'brakeman', require: false gem "annotate", "~> 2.6.0.beta2" gem "letter_opener" gem 'quiet_assets', '~> 1.0.1' gem 'rack-mini-profiler', require: false + gem 'rerun', '~> 0.10.0' # Better errors handler gem 'better_errors' gem 'binding_of_caller' - gem 'rails_best_practices' - # Docs generator gem "sdoc" @@ -208,47 +222,41 @@ group :development do end group :development, :test do + gem 'awesome_print' + gem 'byebug' + gem 'pry-rails' + gem 'coveralls', require: false - # gem 'rails-dev-tweaks' - gem 'spinach-rails' - gem "rspec-rails" - gem "capybara", '~> 2.2.1' - gem "pry" - gem "awesome_print" - gem "database_cleaner" - gem "launchy" + gem 'database_cleaner', '~> 1.4.0' gem 'factory_girl_rails' + gem 'rspec-rails', '~> 3.3.0' + gem 'rubocop', '0.28.0', require: false + gem 'spinach-rails' # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) gem 'minitest', '~> 5.3.0' # Generate Fake data - gem "ffaker" - - # Guard - gem 'guard-rspec' - gem 'guard-spinach' + gem 'ffaker', '~> 2.0.0' - # Notification - gem 'rb-fsevent', require: darwin_only('rb-fsevent') - gem 'growl', require: darwin_only('growl') - gem 'rb-inotify', require: linux_only('rb-inotify') + gem 'capybara', '~> 2.3.0' + gem 'capybara-screenshot', '~> 1.0.0' + gem 'poltergeist', '~> 1.6.0' - # PhantomJS driver for Capybara - gem 'poltergeist', '~> 1.5.1' + gem 'teaspoon', '~> 1.0.0' + gem 'teaspoon-jasmine' - gem 'jasmine', '2.0.2' - - gem "spring", '1.1.3' - gem "spring-commands-rspec", '1.0.1' - gem "spring-commands-spinach", '1.0.0' + gem 'spring', '~> 1.3.1' + gem 'spring-commands-rspec', '~> 1.0.0' + gem 'spring-commands-spinach', '~> 1.0.0' + gem 'spring-commands-teaspoon', '~> 0.0.2' end group :test do - gem "simplecov", require: false - gem "shoulda-matchers", "~> 2.1.0" - gem 'email_spec' - gem "webmock" + gem 'simplecov', require: false + gem 'shoulda-matchers', '~> 2.8.0', require: false + gem 'email_spec', '~> 1.6.0' + gem 'webmock', '~> 1.21.0' gem 'test_after_commit' end @@ -257,3 +265,6 @@ group :production do end gem "newrelic_rpm" + +gem 'octokit', '3.7.0' +gem "rugments", "~> 1.0.0.beta7" diff --git a/Gemfile.lock b/Gemfile.lock index 7871f49d0bf..b719dd4ab06 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,42 +1,58 @@ GEM remote: https://rubygems.org/ specs: + CFPropertyList (2.3.1) RedCloth (4.2.9) ace-rails-ap (2.0.1) - actionmailer (4.1.1) - actionpack (= 4.1.1) - actionview (= 4.1.1) - mail (~> 2.5.4) - actionpack (4.1.1) - actionview (= 4.1.1) - activesupport (= 4.1.1) + actionmailer (4.1.11) + actionpack (= 4.1.11) + actionview (= 4.1.11) + mail (~> 2.5, >= 2.5.4) + actionpack (4.1.11) + actionview (= 4.1.11) + activesupport (= 4.1.11) rack (~> 1.5.2) rack-test (~> 0.6.2) - actionview (4.1.1) - activesupport (= 4.1.1) + actionview (4.1.11) + activesupport (= 4.1.11) builder (~> 3.1) erubis (~> 2.7.0) - activemodel (4.1.1) - activesupport (= 4.1.1) + activemodel (4.1.11) + activesupport (= 4.1.11) builder (~> 3.1) - activerecord (4.1.1) - activemodel (= 4.1.1) - activesupport (= 4.1.1) + activerecord (4.1.11) + activemodel (= 4.1.11) + activesupport (= 4.1.11) arel (~> 5.0.0) - activesupport (4.1.1) + activeresource (4.0.0) + activemodel (~> 4.0) + activesupport (~> 4.0) + rails-observers (~> 0.1.1) + activesupport (4.1.11) i18n (~> 0.6, >= 0.6.9) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.1) tzinfo (~> 1.1) - acts-as-taggable-on (2.4.1) - rails (>= 3, < 5) - addressable (2.3.5) + acts-as-taggable-on (3.5.0) + activerecord (>= 3.2, < 5) + addressable (2.3.8) annotate (2.6.0) activerecord (>= 2.3.0) rake (>= 0.8.7) arel (5.0.1.20140414130214) - asciidoctor (0.1.4) + asana (0.0.6) + activeresource (>= 3.2.3) + asciidoctor (1.5.2) + ast (2.0.0) + astrolabe (1.3.0) + parser (>= 2.2.0.pre.3, < 3.0) + attr_encrypted (1.3.4) + encryptor (>= 1.3.0) + attr_required (1.0.0) + autoprefixer-rails (5.1.11) + execjs + json awesome_print (1.2.0) axiom-types (0.0.5) descendants_tracker (~> 0.0.1) @@ -47,52 +63,71 @@ GEM erubis (>= 2.6.6) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootstrap-sass (3.0.3.0) - sass (~> 3.2) + bootstrap-sass (3.3.4.1) + autoprefixer-rails (>= 5.0.0.1) + sass (>= 3.2.19) + brakeman (3.0.1) + erubis (~> 2.6) + fastercsv (~> 1.5) + haml (>= 3.0, < 5.0) + highline (~> 1.6.20) + multi_json (~> 1.2) + ruby2ruby (~> 2.1.1) + ruby_parser (~> 3.5.0) + sass (~> 3.0) + terminal-table (~> 1.4) + browser (0.8.0) builder (3.2.2) - capybara (2.2.1) + byebug (3.2.0) + columnize (~> 0.8) + debugger-linecache (~> 1.2) + cal-heatmap-rails (0.0.1) + capybara (2.3.0) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + capybara-screenshot (1.0.9) + capybara (>= 1.0, < 3) + launchy carrierwave (0.9.0) activemodel (>= 3.2.0) activesupport (>= 3.2.0) json (>= 1.7) - celluloid (0.15.2) - timers (~> 1.1.0) + celluloid (0.16.0) + timers (~> 4.0.0) charlock_holmes (0.6.9.4) cliver (0.3.2) - code_analyzer (0.4.3) - sexp_processor coderay (1.1.0) coercible (1.0.0) descendants_tracker (~> 0.0.1) - coffee-rails (4.0.1) + coffee-rails (4.1.0) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) - coffee-script (2.2.0) + coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.6.3) + coffee-script-source (1.9.1.1) colored (1.2) colorize (0.5.8) - connection_pool (1.2.0) + columnize (0.9.0) + connection_pool (2.1.0) coveralls (0.7.0) multi_json (~> 1.3) rest-client simplecov (>= 0.7) term-ansicolor thor - crack (0.4.1) - safe_yaml (~> 0.9.0) + crack (0.4.2) + safe_yaml (~> 1.0.0) creole (0.3.8) - d3_rails (3.1.10) + d3_rails (3.5.5) railties (>= 3.1.0) daemons (1.1.9) - database_cleaner (1.3.0) + database_cleaner (1.4.1) debug_inspector (0.0.2) + debugger-linecache (1.2.0) default_value_for (3.0.0) activerecord (>= 3.2.0, < 5.0) descendants_tracker (0.0.3) @@ -104,25 +139,33 @@ GEM warden (~> 1.2.3) devise-async (0.9.0) devise (~> 3.2) + devise-two-factor (1.0.1) + activemodel + activesupport + attr_encrypted (~> 1.3.2) + devise (~> 3.2.4) + rails + rotp (~> 1.6.1) diff-lcs (1.2.5) diffy (3.0.3) docile (1.1.5) + doorkeeper (2.1.3) + railties (>= 3.2) dotenv (0.9.0) dropzonejs-rails (0.4.14) rails (> 3.1) - email_spec (1.5.0) + email_spec (1.6.0) launchy (~> 2.1) mail (~> 2.2) - emoji (1.0.1) - json + encryptor (1.3.0) enumerize (0.7.0) activesupport (>= 3.2) equalizer (0.0.8) erubis (2.7.0) escape_utils (0.2.4) - eventmachine (1.0.3) - excon (0.32.1) - execjs (2.0.2) + eventmachine (1.0.4) + excon (0.45.3) + execjs (2.5.2) expression_parser (0.9.0) factory_girl (4.3.0) activesupport (>= 3.0.0) @@ -133,68 +176,114 @@ GEM multipart-post (~> 1.2.0) faraday_middleware (0.9.0) faraday (>= 0.7.4, < 0.9) - ffaker (1.22.1) - ffi (1.9.3) - fog (1.21.0) - fog-brightbox - fog-core (~> 1.21, >= 1.21.1) + fastercsv (1.5.5) + ffaker (2.0.0) + ffi (1.9.8) + fission (0.5.0) + CFPropertyList (~> 2.2) + fog (1.25.0) + fog-brightbox (~> 0.4) + fog-core (~> 1.25) fog-json + fog-profitbricks + fog-radosgw (>= 0.0.2) + fog-sakuracloud (>= 0.0.4) + fog-softlayer + fog-terremark + fog-vmfusion + fog-voxel + fog-xml (~> 0.1.1) + ipaddress (~> 0.5) nokogiri (~> 1.5, >= 1.5.11) - fog-brightbox (0.0.1) - fog-core + opennebula + fog-brightbox (0.7.1) + fog-core (~> 1.22) fog-json - fog-core (1.21.1) + inflecto (~> 0.0.2) + fog-core (1.30.0) builder - excon (~> 0.32) - formatador (~> 0.2.0) + excon (~> 0.45) + formatador (~> 0.2) mime-types net-scp (~> 1.1) net-ssh (>= 2.1.3) - fog-json (1.0.0) - multi_json (~> 1.0) + fog-json (1.0.2) + fog-core (~> 1.0) + multi_json (~> 1.10) + fog-profitbricks (0.0.3) + fog-core + fog-xml + nokogiri + fog-radosgw (0.0.4) + fog-core (>= 1.21.0) + fog-json + fog-xml (>= 0.0.1) + fog-sakuracloud (1.0.1) + fog-core + fog-json + fog-softlayer (0.4.6) + 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-xml (0.1.2) + fog-core + nokogiri (~> 1.5, >= 1.5.11) font-awesome-rails (4.2.0.0) railties (>= 3.2, < 5.0) foreman (0.63.0) dotenv (>= 0.7) thor (>= 0.13.6) - formatador (0.2.4) - gemnasium-gitlab-service (0.2.2) - rugged (~> 0.19) + formatador (0.2.5) + gemnasium-gitlab-service (0.2.6) + rugged (~> 0.21) + gemojione (2.0.0) + json gherkin-ruby (0.3.1) racc - github-markup (1.1.0) + github-markup (1.3.1) + posix-spawn (~> 0.3.8) gitlab-flowdock-git-hook (0.4.2.2) gitlab-grit (>= 2.4.1) multi_json - gitlab-grack (2.0.0.pre) + gitlab-grack (2.0.2) rack (~> 1.5.1) - gitlab-grit (2.6.12) + gitlab-grit (2.7.2) charlock_holmes (~> 0.6) diff-lcs (~> 1.1) mime-types (~> 1.15) posix-spawn (~> 0.3) - gitlab-linguist (3.0.0) + gitlab-linguist (3.0.1) charlock_holmes (~> 0.6.6) escape_utils (~> 0.2.4) mime-types (~> 1.19) - gitlab_emoji (0.0.1.1) - emoji (~> 1.0.1) - gitlab_git (7.0.0.rc12) + gitlab_emoji (0.1.0) + gemojione (~> 2.0) + gitlab_git (7.2.5) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) - rugged (~> 0.21.2) + rugged (~> 0.22.2) gitlab_meta (7.0) - gitlab_omniauth-ldap (1.2.0) + gitlab_omniauth-ldap (1.2.1) net-ldap (~> 0.9) omniauth (~> 1.0) pyu-ruby-sasl (~> 0.0.3.1) rubyntlm (~> 0.3) - gollum-lib (3.0.0) - github-markup (~> 1.1.0) - gitlab-grit (~> 2.6.5) - nokogiri (~> 1.6.1) - rouge (~> 1.3.3) + gollum-grit_adapter (0.1.3) + gitlab-grit (~> 2.7, >= 2.7.1) + gollum-lib (4.0.2) + github-markup (~> 1.3.1) + gollum-grit_adapter (~> 0.1, >= 0.1.1) + nokogiri (~> 1.6.4) + rouge (~> 1.7.4) sanitize (~> 2.1.0) stringex (~> 2.5.1) gon (5.0.1) @@ -213,19 +302,6 @@ GEM grape-entity (0.4.2) activesupport multi_json (>= 1.3.2) - growl (1.0.3) - guard (2.2.4) - formatador (>= 0.2.4) - listen (~> 2.1) - lumberjack (~> 1.0) - pry (>= 0.9.12) - thor (>= 0.18.1) - guard-rspec (4.2.0) - guard (>= 2.1.1) - rspec (>= 2.14, < 4.0) - guard-spinach (0.0.2) - guard (>= 1.1) - spinach haml (4.0.5) tilt haml-rails (0.5.3) @@ -234,32 +310,28 @@ GEM haml (>= 3.1, < 5.0) railties (>= 4.0.1) hashie (2.1.2) + highline (1.6.21) hike (1.2.3) - hipchat (1.4.0) + hipchat (1.5.0) httparty + mimemagic + hitimes (1.2.2) html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) - html-pipeline-gitlab (0.1.5) - actionpack (~> 4) - gitlab_emoji (~> 0.0.1) - html-pipeline (~> 1.11.0) - sanitize (~> 2.1) http_parser.rb (0.5.3) - httparty (0.13.0) + httparty (0.13.3) json (~> 1.8) multi_xml (>= 0.5.2) httpauth (0.2.1) - i18n (0.6.11) + httpclient (2.5.3.3) + i18n (0.7.0) + ice_cube (0.11.1) ice_nine (0.10.0) - jasmine (2.0.2) - jasmine-core (~> 2.0.0) - phantomjs - rack (>= 1.2.1) - rake - jasmine-core (2.0.0) - jquery-atwho-rails (0.3.3) - jquery-rails (3.1.0) + inflecto (0.0.2) + ipaddress (0.8.0) + jquery-atwho-rails (1.0.1) + jquery-rails (3.1.3) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) jquery-scrollto-rails (1.4.3) @@ -269,42 +341,42 @@ GEM turbolinks jquery-ui-rails (4.2.1) railties (>= 3.2.16) - json (1.8.1) + json (1.8.3) jwt (0.1.13) multi_json (>= 1.5) kaminari (0.15.1) actionpack (>= 3.0.0) activesupport (>= 3.0.0) - kgio (2.8.1) - launchy (2.4.2) + kgio (2.9.2) + launchy (2.4.3) addressable (~> 2.3) letter_opener (1.1.2) launchy (~> 2.2) - libv8 (3.16.14.7) - listen (2.3.1) - celluloid (>= 0.15.2) + listen (2.10.0) + celluloid (~> 0.16.0) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) - lumberjack (1.0.4) - mail (2.5.4) - mime-types (~> 1.16) - treetop (~> 1.4.8) + macaddr (1.7.1) + systemu (~> 2.6.2) + mail (2.6.3) + mime-types (>= 1.16, < 3) method_source (0.8.2) mime-types (1.25.1) - mini_portile (0.6.0) + mimemagic (0.3.0) + mini_portile (0.6.2) minitest (5.3.5) mousetrap-rails (1.4.6) - multi_json (1.10.1) + multi_json (1.11.1) multi_xml (0.5.5) multipart-post (1.2.0) mysql2 (0.3.16) - net-ldap (0.9.0) - net-scp (1.1.2) + net-ldap (0.11) + net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (2.8.0) + net-ssh (2.9.2) newrelic_rpm (3.9.4.245) - nokogiri (1.6.2.1) - mini_portile (= 0.6.0) + nokogiri (1.6.6.2) + mini_portile (~> 0.6.0) nprogress-rails (0.1.2.3) oauth (0.4.7) oauth2 (0.8.1) @@ -313,96 +385,122 @@ GEM jwt (~> 0.1.4) multi_json (~> 1.0) rack (~> 1.2) - omniauth (1.1.4) - hashie (>= 1.2, < 3) - rack + octokit (3.7.0) + sawyer (~> 0.6.0, >= 0.5.3) + omniauth (1.2.2) + hashie (>= 1.2, < 4) + rack (~> 1.0) + omniauth-bitbucket (0.0.2) + multi_json (~> 1.7) + omniauth (~> 1.1) + omniauth-oauth (~> 1.0) omniauth-github (1.1.1) omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) + omniauth-gitlab (1.0.0) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.0) omniauth-google-oauth2 (0.2.5) omniauth (> 1.0) omniauth-oauth2 (~> 1.1) + omniauth-kerberos (0.2.0) + omniauth-multipassword + timfel-krb5-auth (~> 0.8) + omniauth-multipassword (0.4.1) + omniauth (~> 1.0) omniauth-oauth (1.0.1) oauth omniauth (~> 1.0) omniauth-oauth2 (1.1.1) oauth2 (~> 0.8.0) omniauth (~> 1.0) + omniauth-saml (1.3.1) + omniauth (~> 1.1) + ruby-saml (~> 0.8.1) omniauth-shibboleth (1.1.1) omniauth (>= 1.0.0) omniauth-twitter (1.0.1) multi_json (~> 1.3) omniauth-oauth (~> 1.0) - org-ruby (0.9.9) + opennebula (4.12.1) + json + nokogiri + rbvmomi + org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) - pg (0.15.1) - phantomjs (1.9.2.0) - poltergeist (1.5.1) + parser (2.2.0.2) + ast (>= 1.1, < 3.0) + pg (0.18.2) + poltergeist (1.6.0) capybara (~> 2.1) cliver (~> 0.3.1) multi_json (~> 1.0) websocket-driver (>= 0.2.0) - polyglot (0.3.4) posix-spawn (0.3.9) + powerpack (0.0.9) pry (0.9.12.4) coderay (~> 1.0) method_source (~> 0.8) slop (~> 3.4) + pry-rails (0.3.2) + pry (>= 0.9.10) pyu-ruby-sasl (0.0.3.3) quiet_assets (1.0.2) railties (>= 3.1, < 5.0) racc (1.4.10) - rack (1.5.2) + rack (1.5.5) rack-accept (0.4.5) rack (>= 0.4) - rack-attack (2.3.0) + rack-attack (4.3.0) rack rack-cors (0.2.9) rack-mini-profiler (0.9.0) rack (>= 1.1.3) rack-mount (0.8.3) rack (>= 1.0.0) + rack-oauth2 (1.0.8) + activesupport (>= 2.3) + attr_required (>= 0.0.5) + httpclient (>= 2.2.0.2) + multi_json (>= 1.3.6) + rack (>= 1.1) rack-protection (1.5.1) rack - rack-test (0.6.2) + rack-test (0.6.3) rack (>= 1.0) - rails (4.1.1) - actionmailer (= 4.1.1) - actionpack (= 4.1.1) - actionview (= 4.1.1) - activemodel (= 4.1.1) - activerecord (= 4.1.1) - activesupport (= 4.1.1) + rails (4.1.11) + actionmailer (= 4.1.11) + actionpack (= 4.1.11) + actionview (= 4.1.11) + activemodel (= 4.1.11) + activerecord (= 4.1.11) + activesupport (= 4.1.11) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.1) + railties (= 4.1.11) sprockets-rails (~> 2.0) - rails_autolink (1.1.6) - rails (> 3.1) - rails_best_practices (1.14.4) - activesupport - awesome_print - code_analyzer (>= 0.4.3) - colored - erubis - i18n - require_all - ruby-progressbar - railties (4.1.1) - actionpack (= 4.1.1) - activesupport (= 4.1.1) + rails-observers (0.1.2) + activemodel (~> 4.0) + railties (4.1.11) + actionpack (= 4.1.11) + activesupport (= 4.1.11) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - raindrops (0.12.0) - rake (10.3.2) + rainbow (2.0.0) + raindrops (0.13.0) + rake (10.4.2) raphael-rails (2.1.2) - rb-fsevent (0.9.3) - rb-inotify (0.9.2) + rb-fsevent (0.9.4) + rb-inotify (0.9.5) ffi (>= 0.5.0) + rbvmomi (1.8.2) + builder + nokogiri (>= 1.4.1) + trollop rdoc (3.12.2) json (~> 1.4) - redcarpet (3.1.2) - redis (3.0.6) + redcarpet (3.3.1) + redis (3.1.0) redis-actionpack (4.0.0) actionpack (~> 4) redis-rack (~> 1.5.0) @@ -410,8 +508,8 @@ GEM redis-activesupport (4.0.0) activesupport (~> 4) redis-store (~> 1.1.0) - redis-namespace (1.4.1) - redis (~> 3.0.4) + redis-namespace (1.5.1) + redis (~> 3.0, >= 3.0.4) redis-rack (1.5.0) rack (~> 1.5) redis-store (~> 1.1.0) @@ -421,33 +519,54 @@ GEM redis-store (~> 1.1.0) redis-store (1.1.4) redis (>= 2.2) - ref (1.0.5) request_store (1.0.5) - require_all (1.3.2) + rerun (0.10.0) + listen (~> 2.7, >= 2.7.3) rest-client (1.6.7) mime-types (>= 1.16) rinku (1.7.3) - rouge (1.3.3) - rspec (2.14.1) - rspec-core (~> 2.14.0) - rspec-expectations (~> 2.14.0) - rspec-mocks (~> 2.14.0) - rspec-core (2.14.7) - rspec-expectations (2.14.4) - diff-lcs (>= 1.1.3, < 2.0) - rspec-mocks (2.14.4) - rspec-rails (2.14.0) - actionpack (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-core (~> 2.14.0) - rspec-expectations (~> 2.14.0) - rspec-mocks (~> 2.14.0) - ruby-progressbar (1.2.0) - rubyntlm (0.4.0) + rotp (1.6.1) + rouge (1.7.7) + rqrcode (0.4.2) + rqrcode-rails3 (0.1.7) + rqrcode (>= 0.4.2) + rspec-core (3.3.1) + rspec-support (~> 3.3.0) + rspec-expectations (3.3.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.3.0) + rspec-mocks (3.3.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.3.0) + rspec-rails (3.3.2) + actionpack (>= 3.0, < 4.3) + activesupport (>= 3.0, < 4.3) + railties (>= 3.0, < 4.3) + rspec-core (~> 3.3.0) + rspec-expectations (~> 3.3.0) + rspec-mocks (~> 3.3.0) + rspec-support (~> 3.3.0) + rspec-support (3.3.0) + rubocop (0.28.0) + astrolabe (~> 1.3) + parser (>= 2.2.0.pre.7, < 3.0) + powerpack (~> 0.0.6) + rainbow (>= 1.99.1, < 3.0) + ruby-progressbar (~> 1.4) + ruby-progressbar (1.7.1) + ruby-saml (0.8.2) + nokogiri (>= 1.5.0) + uuid (~> 2.3) + ruby2ruby (2.1.3) + ruby_parser (~> 3.1) + sexp_processor (~> 4.0) + ruby_parser (3.5.0) + sexp_processor (~> 4.1) + rubyntlm (0.5.0) rubypants (0.2.0) - rugged (0.21.2) - safe_yaml (0.9.7) + rugged (0.22.2) + rugments (1.0.0.beta7) + safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) sass (3.2.19) @@ -456,26 +575,31 @@ GEM sass (~> 3.2.0) sprockets (~> 2.8, <= 2.11.0) sprockets-rails (~> 2.0) + sawyer (0.6.0) + addressable (~> 2.3.5) + faraday (~> 0.8, < 0.10) sdoc (0.3.20) json (>= 1.1.3) rdoc (~> 3.10) - seed-fu (2.3.1) - activerecord (>= 3.1, < 4.2) - activesupport (>= 3.1, < 4.2) + seed-fu (2.3.5) + activerecord (>= 3.1, < 4.3) + activesupport (>= 3.1, < 4.3) select2-rails (3.5.2) thor (~> 0.14) - semantic-ui-sass (0.16.1.0) - sass (~> 3.2) settingslogic (2.0.9) - sexp_processor (4.4.0) - shoulda-matchers (2.1.0) + sexp_processor (4.4.5) + shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (2.17.0) - celluloid (>= 0.15.2) - connection_pool (>= 1.0.0) + sidekiq (3.3.0) + celluloid (>= 0.16.0) + connection_pool (>= 2.0.0) json - redis (>= 3.0.4) + redis (>= 3.0.6) redis-namespace (>= 1.3.1) + sidetiq (0.6.3) + celluloid (>= 0.14.1) + ice_cube (= 0.11.1) + sidekiq (>= 3.0.0) simple_oauth (0.1.9) simplecov (0.9.0) docile (~> 1.1.0) @@ -491,7 +615,7 @@ GEM slim (2.0.2) temple (~> 0.6.6) tilt (>= 1.3.3, < 2.1) - slop (3.4.7) + slop (3.6.0) spinach (0.8.7) colorize (= 0.5.8) gherkin-ruby (>= 0.3.1) @@ -499,38 +623,47 @@ GEM capybara (>= 2.0.0) railties (>= 3) spinach (>= 0.4) - spring (1.1.3) - spring-commands-rspec (1.0.1) + spring (1.3.6) + spring-commands-rspec (1.0.4) spring (>= 0.9.1) spring-commands-spinach (1.0.0) spring (>= 0.9.1) + spring-commands-teaspoon (0.0.2) + spring (>= 0.9.1) sprockets (2.11.0) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.1.3) + sprockets-rails (2.3.1) actionpack (>= 3.0) activesupport (>= 3.0) - sprockets (~> 2.8) + sprockets (>= 2.8, < 4.0) stamp (0.5.0) state_machine (1.2.0) - stringex (2.5.1) + stringex (2.5.2) + systemu (2.6.5) + task_list (1.0.2) + html-pipeline + teaspoon (1.0.2) + railties (>= 3.2.5, < 5) + teaspoon-jasmine (2.2.0) + teaspoon (>= 1.0.0) temple (0.6.7) term-ansicolor (1.2.2) tins (~> 0.8) + terminal-table (1.4.5) test_after_commit (0.2.2) - therubyracer (0.12.0) - libv8 (~> 3.16.14.0) - ref thin (1.6.1) daemons (>= 1.0.9) eventmachine (>= 1.0.0) rack (>= 1.0.0) thor (0.19.1) - thread_safe (0.3.4) + thread_safe (0.3.5) tilt (1.4.1) - timers (1.1.0) + timers (4.0.1) + hitimes + timfel-krb5-auth (0.8.3) tinder (1.9.3) eventmachine (~> 1.0) faraday (~> 0.8) @@ -541,10 +674,8 @@ GEM multi_json (~> 1.7) twitter-stream (~> 0.1) tins (0.13.1) - treetop (1.4.15) - polyglot - polyglot (>= 0.3.1) - turbolinks (2.0.0) + trollop (2.1.2) + turbolinks (2.5.3) coffee-rails twitter-stream (0.1.16) eventmachine (>= 0.12.8) @@ -565,7 +696,9 @@ GEM raindrops (~> 0.7) unicorn-worker-killer (0.4.2) unicorn (~> 4) - version_sorter (1.1.0) + uuid (2.3.7) + macaddr (~> 1.0) + version_sorter (2.0.0) virtus (1.0.1) axiom-types (~> 0.0.5) coercible (~> 1.0) @@ -573,10 +706,12 @@ GEM equalizer (~> 0.0.7) warden (1.2.3) rack (>= 1.0) - webmock (1.16.0) - addressable (>= 2.2.7) + webmock (1.21.0) + addressable (>= 2.3.6) crack (>= 0.3.2) - websocket-driver (0.3.3) + websocket-driver (0.5.4) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) wikicloth (0.8.1) builder expression_parser @@ -590,117 +725,131 @@ PLATFORMS DEPENDENCIES RedCloth ace-rails-ap - acts-as-taggable-on + acts-as-taggable-on (~> 3.4) addressable annotate (~> 2.6.0.beta2) - asciidoctor (= 0.1.4) + asana (~> 0.0.6) + asciidoctor (~> 1.5.2) + attr_encrypted (= 1.3.4) awesome_print better_errors binding_of_caller bootstrap-sass (~> 3.0) - capybara (~> 2.2.1) + brakeman + browser (~> 0.8.0) + byebug + cal-heatmap-rails (~> 0.0.1) + capybara (~> 2.3.0) + capybara-screenshot (~> 1.0.0) carrierwave + charlock_holmes coffee-rails colored coveralls creole (~> 0.3.6) - d3_rails (~> 3.1.4) - database_cleaner + d3_rails (~> 3.5.5) + database_cleaner (~> 1.4.0) default_value_for (~> 3.0.0) devise (= 3.2.4) devise-async (= 0.9.0) + devise-two-factor diffy (~> 3.0.3) + doorkeeper (= 2.1.3) dropzonejs-rails - email_spec + email_spec (~> 1.6.0) enumerize factory_girl_rails - ffaker - fog (~> 1.14) + ffaker (~> 2.0.0) + fog (~> 1.25.0) font-awesome-rails (~> 4.2) foreman gemnasium-gitlab-service (~> 0.2) github-markup gitlab-flowdock-git-hook (~> 0.4.2) - gitlab-grack (~> 2.0.0.pre) - gitlab-linguist (~> 3.0.0) - gitlab_emoji (~> 0.0.1.1) - gitlab_git (= 7.0.0.rc12) + gitlab-grack (~> 2.0.2) + gitlab-linguist (~> 3.0.1) + gitlab_emoji (~> 0.1) + gitlab_git (~> 7.2.5) gitlab_meta (= 7.0) - gitlab_omniauth-ldap (= 1.2.0) - gollum-lib (~> 3.0.0) + gitlab_omniauth-ldap (= 1.2.1) + gollum-lib (~> 4.0.2) gon (~> 5.0.0) grape (~> 0.6.1) grape-entity (~> 0.4.2) - growl - guard-rspec - guard-spinach haml-rails - hipchat (~> 1.4.0) - html-pipeline-gitlab (~> 0.1.0) + hipchat (~> 1.5.0) + html-pipeline (~> 1.11.0) httparty - jasmine (= 2.0.2) - jquery-atwho-rails (~> 0.3.3) - jquery-rails + jquery-atwho-rails (~> 1.0.0) + jquery-rails (= 3.1.3) jquery-scrollto-rails jquery-turbolinks jquery-ui-rails kaminari (~> 0.15.1) - launchy letter_opener minitest (~> 5.3.0) mousetrap-rails mysql2 newrelic_rpm nprogress-rails - omniauth (~> 1.1.3) + octokit (= 3.7.0) + omniauth (~> 1.2.2) + omniauth-bitbucket omniauth-github + omniauth-gitlab omniauth-google-oauth2 + omniauth-kerberos + omniauth-saml omniauth-shibboleth omniauth-twitter - org-ruby (= 0.9.9) + org-ruby (= 0.9.12) pg - poltergeist (~> 1.5.1) - pry + poltergeist (~> 1.6.0) + pry-rails quiet_assets (~> 1.0.1) - rack-attack + rack-attack (~> 4.3.0) rack-cors rack-mini-profiler - rails (~> 4.1.0) - rails_autolink (~> 1.1) - rails_best_practices + rack-oauth2 (~> 1.0.5) + rails (= 4.1.11) raphael-rails (~> 2.1.2) - rb-fsevent - rb-inotify rdoc (~> 3.6) - redcarpet (~> 3.1.2) + redcarpet (~> 3.3.0) redis-rails request_store - rspec-rails + rerun (~> 0.10.0) + rqrcode-rails3 + rspec-rails (~> 3.3.0) + rubocop (= 0.28.0) + rugments (~> 1.0.0.beta7) sanitize (~> 2.0) sass-rails (~> 4.0.2) sdoc seed-fu select2-rails - semantic-ui-sass (~> 0.16.1.0) settingslogic - shoulda-matchers (~> 2.1.0) - sidekiq (= 2.17.0) + shoulda-matchers (~> 2.8.0) + sidekiq (~> 3.3) + sidetiq (= 0.6.3) simplecov sinatra six slack-notifier (~> 1.0.0) slim spinach-rails - spring (= 1.1.3) - spring-commands-rspec (= 1.0.1) - spring-commands-spinach (= 1.0.0) + spring (~> 1.3.1) + spring-commands-rspec (~> 1.0.0) + spring-commands-spinach (~> 1.0.0) + spring-commands-teaspoon (~> 0.0.2) stamp state_machine + task_list (= 1.0.2) + teaspoon (~> 1.0.0) + teaspoon-jasmine test_after_commit - therubyracer thin tinder (~> 1.9.2) - turbolinks + turbolinks (~> 2.5.0) uglifier underscore-rails (~> 1.4.4) unf @@ -708,5 +857,5 @@ DEPENDENCIES unicorn-worker-killer version_sorter virtus - webmock + webmock (~> 1.21.0) wikicloth (= 0.8.1) diff --git a/Guardfile b/Guardfile deleted file mode 100644 index 68ac3232b09..00000000000 --- a/Guardfile +++ /dev/null @@ -1,27 +0,0 @@ -# A sample Guardfile -# More info at https://github.com/guard/guard#readme - -guard 'rspec', cmd: "spring rspec", all_on_start: false, all_after_pass: false do - watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } - watch(%r{^lib/api/(.+)\.rb$}) { |m| "spec/requests/api/#{m[1]}_spec.rb" } - watch('spec/spec_helper.rb') { "spec" } - - # Rails example - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } - watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } - watch(%r{^spec/support/(.+)\.rb$}) { "spec" } - watch('config/routes.rb') { "spec/routing" } - watch('app/controllers/application_controller.rb') { "spec/controllers" } - - # Capybara request specs - watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } -end - -guard 'spinach', command_prefix: 'spring' do - watch(%r|^features/(.*)\.feature|) - watch(%r|^features/steps/(.*)([^/]+)\.rb|) do |m| - "features/#{m[1]}#{m[2]}.feature" - end -end @@ -1,4 +1,4 @@ -Copyright (c) 2011-2014 GitLab B.V. +Copyright (c) 2011-2015 GitLab B.V. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 19200fef389..d3d36670693 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -2,7 +2,7 @@ GitLab is a fast moving and evolving project. We currently don't have the resources to support many releases concurrently. We support exactly one stable release at any given time. -GitLab follows the [Semantic Versioning](http://semver.org/) for its releases: `(Major).(Minor).(Patch)`. +GitLab follows the [Semantic Versioning](http://semver.org/) for its releases: `(Major).(Minor).(Patch)` in a [pragmatic way](https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e). - **Major version**: Whenever there is something significant or any backwards incompatible changes are introduced to the public API. - **Minor version**: When new, backwards compatible functionality is introduced to the public API or a minor feature is introduced, or when a set of smaller features is rolled out. diff --git a/PROCESS.md b/PROCESS.md index 1dd28d6b670..1b6b3e7d32d 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -71,7 +71,7 @@ Thanks for the issue report. Please reformat your issue to conform to the issue ### Feature requests -Thank you for your interest in improving GitLab. We don't use the issue tracker for feature requests. Things that are wrong but are not a regression compared to older versions of GitLab are considered feature requests and not issues. Please use the [feature request forum](http://feedback.gitlab.com/) for this purpose or create a merge request implementing this feature. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information. +Thank you for your interest in improving GitLab. We don't use the issue tracker for feature requests. Things that are wrong but are not a regression compared to older versions of GitLab are considered feature requests and not issues. Please use the \[feature request forum\]\(http://feedback.gitlab.com/) for this purpose or create a merge request implementing this feature. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information. ### Issue report for old version @@ -104,3 +104,10 @@ This merge request has been closed because a request for more information has no ### Accepting merge requests Is there a request on [the feature request forum](http://feedback.gitlab.com/forums/176466-general) that is similar to this? If so, can you make a comment with a link to it? Please be aware that new functionality that is not marked [accepting merge/pull requests](http://feedback.gitlab.com/forums/176466-general/status/796455) on the forum might not make it into GitLab. You might be asked to make changes and even after implementing them your feature might still be declined. If you want to reduce the chance of this happening please have a discussion in the forum first. + +### Only accepting merge requests with green tests + +We can only accept a merge request if all the tests are green. I've just +restarted the build. When the tests are still not passing after this restart and +you're sure that is does not have anything to do with your code changes, please +rebase with master to see if that solves the issue. @@ -1,2 +1,2 @@ web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"} -worker: bundle exec sidekiq -q post_receive,mailer,system_hook,project_web_hook,common,default,gitlab_shell +worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q common -q default diff --git a/README.md b/README.md index 63fa5e3da86..85ea5c876af 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,27 @@ +## Canonical source + +The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/) and there are mirrors to make [contributing](CONTRIBUTING.md) as easy as possible. + #  GitLab ## Open source software to collaborate on code - +To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/). - Manage Git repositories with fine grained access controls that keep your code secure - Perform code reviews and enhance collaboration with merge requests - Each project can also have an issue tracker and a wiki - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises - Completely free and open source (MIT Expat license) -- Powered by Ruby on Rails +- Powered by [Ruby on Rails](https://github.com/rails/rails) -## Canonical source +## Editions -- The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/) and there are mirrors to make [contributing](CONTRIBUTING.md) as easy as possible. +There are two editions of GitLab. +*GitLab [Community Edition](https://about.gitlab.com/features/) (CE)* is available without any costs under an MIT license. + +*GitLab Enterprise Edition (EE)* includes [extra features](https://about.gitlab.com/features/#compare) that are most useful for organizations with more than 100 users. +To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/). ## Code status @@ -25,8 +33,6 @@ - [](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) -- [](https://www.pullreview.com/gitlab.gitlab.com/gitlab-org/gitlab-ce/reviews/master) - ## Website On [about.gitlab.com](https://about.gitlab.com/) you can find more information about: @@ -40,81 +46,45 @@ On [about.gitlab.com](https://about.gitlab.com/) you can find more information a ## Requirements -- Ubuntu/Debian/CentOS/RHEL** -- ruby 2.0+ -- git 1.7.10+ -- redis 2.0+ +GitLab requires the following software: + +- Ubuntu/Debian/CentOS/RHEL +- Ruby (MRI) 2.0 or 2.1 +- Git 1.7.10+ +- Redis 2.0+ - MySQL or PostgreSQL -** More details are in the [requirements doc](doc/install/requirements.md). +Please see the [requirements documentation](doc/install/requirements.md) for system requirements and more information about the supported operating systems. ## Installation -Please see [the installation page on the GitLab website](https://about.gitlab.com/installation/) for the various options. -Since a manual installation is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm). +The recommended way to install GitLab is using the provided [Omnibus packages](https://about.gitlab.com/downloads/). Compared to an installation from source, this is faster and less error prone. Just select your operating system, download the respective package (Debian or RPM) and install it using the system's package manager. -## Third-party applications - -There are a lot of applications and API wrappers for GitLab. -Find them [on our website](https://about.gitlab.com/applications/). - -### New versions - -Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases come out when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). For more information about the release process see the release [documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/release). Features that will likely be in the next releases can be found on the [feature request forum](http://feedback.gitlab.com/forums/176466-general) with the status [started](http://feedback.gitlab.com/forums/176466-general/status/796456) and [completed](http://feedback.gitlab.com/forums/176466-general/status/796457). +There are various other options to install GitLab, please refer to the [installation page on the GitLab website](https://about.gitlab.com/installation/) for more information. -### Upgrading +You can access a new installation with the login **`root`** and password **`5iveL!fe`**, after login you are required to set a unique password. -For updating the the Omnibus installation please see the [update documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md). For manual installations there is an [upgrader script](doc/update/upgrader.md) and there are [upgrade guides](doc/update). - -## Run in production mode +## Third-party applications -The Installation guide contains instructions on how to download an init script and run it automatically on boot. You can also start the init script manually: +There are a lot of [third-party applications integrating with GitLab](https://about.gitlab.com/applications/). These include GUI Git clients, mobile applications and API wrappers for various languages. - sudo service gitlab start +## GitLab release cycle -or by directly calling the script: +Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). For more information about the release process see the [release documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/release). Features that will likely be in the next releases can be found on the [feature request forum](http://feedback.gitlab.com/forums/176466-general) with the status [started](http://feedback.gitlab.com/forums/176466-general/status/796456) and [completed](http://feedback.gitlab.com/forums/176466-general/status/796457). - sudo /etc/init.d/gitlab start +## Upgrading -Please login with `root` / `5iveL!fe` +For updating the Omnibus installation please see the [update documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md). For installations from source there is an [upgrader script](doc/update/upgrader.md) and there are [upgrade guides](doc/update) detailing all necessary commands to migrate to the next version. ## Install a development environment -We recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). -If you do not use the development kit you might need to copy the example development unicorn configuration file +To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). +If you do not use the GitLab Development Kit you need to install and setup all the dependencies yourself, this is a lot of work and error prone. +One small thing you also have to do when installing it yourself is to copy the example development unicorn configuration file: cp config/unicorn.rb.example.development config/unicorn.rb -## Run in development mode - -Start it with [Foreman](https://github.com/ddollar/foreman) - - bundle exec foreman start -p 3000 - -or start each component separately: - - bundle exec rails s - bin/background_jobs start - -And surf to [localhost:3000](http://localhost:3000/) and login with `root` / `5iveL!fe`. - -## Run the tests - -- Run all tests: - - bundle exec rake test - -- [RSpec](http://rspec.info/) unit and functional tests. - - All RSpec tests: `bundle exec rake spec` - - Single RSpec file: `bundle exec rspec spec/controllers/commit_controller_spec.rb` - -- [Spinach](https://github.com/codegram/spinach) integration tests. - - All Spinach tests: `bundle exec rake spinach` - - Single Spinach test: `bundle exec spinach features/project/issues/milestones.feature` +Instructions on how to start GitLab and how to run the tests can be found in the [development section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#development). ## Documentation @@ -131,4 +101,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on ## Is it awesome? Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua. -[These people](https://twitter.com/gitlabhq/favorites) seem to like it. +[These people](https://twitter.com/gitlab/favorites) seem to like it.
\ No newline at end of file @@ -1 +1 @@ -7.6.0.pre +7.12.0.pre
\ No newline at end of file diff --git a/app/assets/images/authbuttons/bitbucket_64.png b/app/assets/images/authbuttons/bitbucket_64.png Binary files differnew file mode 100644 index 00000000000..4b90a57bc7d --- /dev/null +++ b/app/assets/images/authbuttons/bitbucket_64.png diff --git a/app/assets/images/authbuttons/github_32.png b/app/assets/images/authbuttons/github_32.png Binary files differdeleted file mode 100644 index c56eef05eb9..00000000000 --- a/app/assets/images/authbuttons/github_32.png +++ /dev/null diff --git a/app/assets/images/authbuttons/github_64.png b/app/assets/images/authbuttons/github_64.png Binary files differindex 39de55bc796..dc7c03d1005 100644 --- a/app/assets/images/authbuttons/github_64.png +++ b/app/assets/images/authbuttons/github_64.png diff --git a/app/assets/images/authbuttons/gitlab_64.png b/app/assets/images/authbuttons/gitlab_64.png Binary files differnew file mode 100644 index 00000000000..31281a19444 --- /dev/null +++ b/app/assets/images/authbuttons/gitlab_64.png diff --git a/app/assets/images/authbuttons/google_32.png b/app/assets/images/authbuttons/google_32.png Binary files differdeleted file mode 100644 index 6225cc9c2d7..00000000000 --- a/app/assets/images/authbuttons/google_32.png +++ /dev/null diff --git a/app/assets/images/authbuttons/google_64.png b/app/assets/images/authbuttons/google_64.png Binary files differindex 4d608f71008..fb64f8bee68 100644 --- a/app/assets/images/authbuttons/google_64.png +++ b/app/assets/images/authbuttons/google_64.png diff --git a/app/assets/images/authbuttons/twitter_32.png b/app/assets/images/authbuttons/twitter_32.png Binary files differdeleted file mode 100644 index 696eb02484d..00000000000 --- a/app/assets/images/authbuttons/twitter_32.png +++ /dev/null diff --git a/app/assets/images/authbuttons/twitter_64.png b/app/assets/images/authbuttons/twitter_64.png Binary files differindex 2893274766f..e3bd9169a34 100644 --- a/app/assets/images/authbuttons/twitter_64.png +++ b/app/assets/images/authbuttons/twitter_64.png diff --git a/app/assets/images/bg-header.png b/app/assets/images/bg-header.png Binary files differindex 9ecdaf4e2d5..639271c6faf 100644 --- a/app/assets/images/bg-header.png +++ b/app/assets/images/bg-header.png diff --git a/app/assets/images/bg_fallback.png b/app/assets/images/bg_fallback.png Binary files differindex d9066ad7d7b..e5fe659ba63 100644 --- a/app/assets/images/bg_fallback.png +++ b/app/assets/images/bg_fallback.png diff --git a/app/assets/images/brand_logo.png b/app/assets/images/brand_logo.png Binary files differindex 09b1689ca45..9c564bb6141 100644 --- a/app/assets/images/brand_logo.png +++ b/app/assets/images/brand_logo.png diff --git a/app/assets/images/chosen-sprite.png b/app/assets/images/chosen-sprite.png Binary files differindex d08e4b7e624..3d936b07d44 100644 --- a/app/assets/images/chosen-sprite.png +++ b/app/assets/images/chosen-sprite.png diff --git a/app/assets/images/dark-scheme-preview.png b/app/assets/images/dark-scheme-preview.png Binary files differindex 6dac6cd8ca1..2ef58e52549 100644 --- a/app/assets/images/dark-scheme-preview.png +++ b/app/assets/images/dark-scheme-preview.png diff --git a/app/assets/images/diff_note_add.png b/app/assets/images/diff_note_add.png Binary files differindex 8ec15b701fc..0084422e330 100644 --- a/app/assets/images/diff_note_add.png +++ b/app/assets/images/diff_note_add.png diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico Binary files differindex bfb74960c48..3479cbbb46f 100644 --- a/app/assets/images/favicon.ico +++ b/app/assets/images/favicon.ico diff --git a/app/assets/images/gitorious-logo-black.png b/app/assets/images/gitorious-logo-black.png Binary files differnew file mode 100644 index 00000000000..78f17a9af79 --- /dev/null +++ b/app/assets/images/gitorious-logo-black.png diff --git a/app/assets/images/gitorious-logo-blue.png b/app/assets/images/gitorious-logo-blue.png Binary files differnew file mode 100644 index 00000000000..4962cffba31 --- /dev/null +++ b/app/assets/images/gitorious-logo-blue.png diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png Binary files differindex 32ade0fe9a3..60021d5ac47 100644 --- a/app/assets/images/icon-link.png +++ b/app/assets/images/icon-link.png diff --git a/app/assets/images/icon-search.png b/app/assets/images/icon-search.png Binary files differindex 084b89e3a7c..3c1c146541d 100644 --- a/app/assets/images/icon-search.png +++ b/app/assets/images/icon-search.png diff --git a/app/assets/images/icon_sprite.png b/app/assets/images/icon_sprite.png Binary files differindex 9ad65fc443b..2e7a5023398 100644 --- a/app/assets/images/icon_sprite.png +++ b/app/assets/images/icon_sprite.png diff --git a/app/assets/images/images.png b/app/assets/images/images.png Binary files differindex da91f6b1f4c..ad146246caf 100644 --- a/app/assets/images/images.png +++ b/app/assets/images/images.png diff --git a/app/assets/images/logo-black.png b/app/assets/images/logo-black.png Binary files differdeleted file mode 100644 index 4a96572d570..00000000000 --- a/app/assets/images/logo-black.png +++ /dev/null diff --git a/app/assets/images/logo-white.png b/app/assets/images/logo-white.png Binary files differdeleted file mode 100644 index bc2ef601a53..00000000000 --- a/app/assets/images/logo-white.png +++ /dev/null diff --git a/app/assets/images/logo.svg b/app/assets/images/logo.svg new file mode 100644 index 00000000000..c09785cb96f --- /dev/null +++ b/app/assets/images/logo.svg @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="210px" height="210px" viewBox="0 0 210 210" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"> + <!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch --> + <title>Slice 1</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"> + <g id="logo" sketch:type="MSLayerGroup" transform="translate(0.000000, 10.000000)"> + <g id="Page-1" sketch:type="MSShapeGroup"> + <g id="Fill-1-+-Group-24"> + <g id="Group-24"> + <g id="Group"> + <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329"></path> + <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26"></path> + <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326"></path> + <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329"></path> + <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26"></path> + <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326"></path> + <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329"></path> + </g> + </g> + </g> + </g> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/app/assets/images/logo_wordmark.svg b/app/assets/images/logo_wordmark.svg new file mode 100644 index 00000000000..a37fe1235cb --- /dev/null +++ b/app/assets/images/logo_wordmark.svg @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="546px" height="194px" viewBox="0 0 546 194" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"> + <!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch --> + <title>Fill 1 + Group 24</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"> + <g id="Fill-1-+-Group-24" sketch:type="MSLayerGroup"> + <g id="Group-24" sketch:type="MSShapeGroup"> + <path d="M316.7906,65.3001 C301.5016,65.3001 292.0046,77.4461 292.0046,97.0001 C292.0046,116.5541 301.5016,128.7001 316.7906,128.7001 C322.5346,128.7001 327.8716,127.0711 332.2226,123.9881 L332.4336,123.8391 L332.4336,101.8711 L310.4336,101.8711 L310.4336,94.0711 L341.4336,94.0711 L341.4336,126.8061 C334.8706,133.1501 326.3546,136.5001 316.7906,136.5001 C296.2666,136.5001 283.0046,120.9951 283.0046,97.0001 C283.0046,73.0051 296.2666,57.5001 316.7906,57.5001 C326.7826,57.5001 335.2176,61.1481 341.2206,68.0561 L335.2246,73.0381 C330.6986,67.9041 324.4986,65.3001 316.7906,65.3001 L316.7906,65.3001 Z M489.8836,135.2501 L482.9356,135.2501 L480.6016,128.8021 L480.0486,129.2991 C479.9716,129.3681 472.2196,136.2501 462.4606,136.2501 C452.6096,136.2501 445.4606,129.6961 445.4606,120.6671 C445.4606,107.5951 456.7446,104.8511 466.2096,104.8511 C473.5836,104.8511 480.1886,106.5111 480.2546,106.5281 L480.8776,106.6871 L480.8776,105.1011 C480.8776,97.9861 476.4356,94.3781 467.6726,94.3781 C462.3646,94.3781 456.7556,95.6891 451.4236,98.1701 L447.8206,91.9581 C452.5266,88.8961 459.6726,85.3781 467.6726,85.3781 C481.5806,85.3781 489.8836,92.9341 489.8836,105.5891 L489.8836,135.2501 Z M470.6886,111.7771 C460.0716,111.7771 454.4606,114.8511 454.4606,120.6671 C454.4606,124.7281 457.5256,127.2501 462.4606,127.2501 C470.5906,127.2501 477.7276,123.9181 480.6626,121.9481 L480.8836,121.8001 L480.8836,112.6201 L480.4676,112.5491 C480.4226,112.5411 475.8766,111.7771 470.6886,111.7771 L470.6886,111.7771 Z M440.4576,127.4501 L440.4576,135.2501 L410.4606,135.2501 L410.4606,61.2501 L419.4606,61.2501 L419.4606,127.4501 L440.4576,127.4501 Z M520.9416,136.5001 C515.0966,136.5001 508.6886,135.6961 501.8926,134.1091 L501.8926,61.2501 L510.8926,61.2501 L510.8926,89.3131 L511.6656,88.8111 C511.7146,88.7791 516.7346,85.5711 523.6536,85.5711 C525.0336,85.5711 526.4146,85.7001 527.7486,85.9521 C539.0936,88.2761 545.8666,97.4301 545.8666,110.4391 C545.8666,125.7831 535.6176,136.5001 520.9416,136.5001 L520.9416,136.5001 Z M521.9426,94.3781 C518.3636,94.3781 514.6196,95.6031 511.1166,97.9191 L510.8926,98.0681 L510.8926,127.9021 L511.3196,127.9651 C514.6986,128.4601 517.9356,128.7121 520.9416,128.7121 C530.3176,128.7121 536.8666,121.1971 536.8666,110.4391 C536.8666,100.2321 531.4266,94.3781 521.9426,94.3781 L521.9426,94.3781 Z M398.4516,86.2501 L398.4516,94.0501 L383.4516,94.0501 L383.4516,116.9501 C383.4516,119.7551 384.5436,122.3921 386.5276,124.3741 C388.5096,126.3581 391.1466,127.4501 393.9516,127.4501 L398.4516,127.4501 L398.4516,135.2501 L393.9516,135.2501 C383.1996,135.2501 374.4516,126.5021 374.4516,115.7501 L374.4516,61.2501 L383.4516,61.2501 L383.4516,86.2501 L398.4516,86.2501 Z M353.4426,66.2501 L362.4426,66.2501 L362.4426,75.2501 L353.4426,75.2501 L353.4426,66.2501 Z M353.4426,86.2501 L362.4426,86.2501 L362.4426,135.2501 L353.4426,135.2501 L353.4426,86.2501 Z" id="Fill-2" fill="#8C929D"></path> + <g id="Group"> + <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329"></path> + <path id="Fill-6" fill="#FC6D26"></path> + <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26"></path> + <path id="Fill-10" fill="#FC6D26"></path> + <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326"></path> + <path id="Fill-14" fill="#FC6D26"></path> + <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329"></path> + <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26"></path> + <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326"></path> + <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329"></path> + </g> + </g> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/app/assets/images/monokai-scheme-preview.png b/app/assets/images/monokai-scheme-preview.png Binary files differindex 3aeed886a02..fbb339c6a91 100644 --- a/app/assets/images/monokai-scheme-preview.png +++ b/app/assets/images/monokai-scheme-preview.png diff --git a/app/assets/images/move.png b/app/assets/images/move.png Binary files differindex 9d2d55ddf0b..6a0567f8f25 100644 --- a/app/assets/images/move.png +++ b/app/assets/images/move.png diff --git a/app/assets/images/no_avatar.png b/app/assets/images/no_avatar.png Binary files differindex dac3ab1bb89..8287acbce13 100644 --- a/app/assets/images/no_avatar.png +++ b/app/assets/images/no_avatar.png diff --git a/app/assets/images/no_group_avatar.png b/app/assets/images/no_group_avatar.png Binary files differindex a97d4515982..bfb31bb2184 100644 --- a/app/assets/images/no_group_avatar.png +++ b/app/assets/images/no_group_avatar.png diff --git a/app/assets/images/slider_handles.png b/app/assets/images/slider_handles.png Binary files differindex a6d477033fa..884378ec96a 100644 --- a/app/assets/images/slider_handles.png +++ b/app/assets/images/slider_handles.png diff --git a/app/assets/images/solarized-dark-scheme-preview.png b/app/assets/images/solarized-dark-scheme-preview.png Binary files differindex ae092ab5213..7ed7336896b 100644 --- a/app/assets/images/solarized-dark-scheme-preview.png +++ b/app/assets/images/solarized-dark-scheme-preview.png diff --git a/app/assets/images/solarized-light-scheme-preview.png b/app/assets/images/solarized-light-scheme-preview.png Binary files differnew file mode 100644 index 00000000000..c50db75449b --- /dev/null +++ b/app/assets/images/solarized-light-scheme-preview.png diff --git a/app/assets/images/switch_icon.png b/app/assets/images/switch_icon.png Binary files differindex 6b8bde41bc9..c6b6c8d9521 100644 --- a/app/assets/images/switch_icon.png +++ b/app/assets/images/switch_icon.png diff --git a/app/assets/images/trans_bg.gif b/app/assets/images/trans_bg.gif Binary files differindex 5f6ed04a43c..1a1c9c15ec7 100644 --- a/app/assets/images/trans_bg.gif +++ b/app/assets/images/trans_bg.gif diff --git a/app/assets/images/white-scheme-preview.png b/app/assets/images/white-scheme-preview.png Binary files differindex d1866e00158..fc4c40b9227 100644 --- a/app/assets/images/white-scheme-preview.png +++ b/app/assets/images/white-scheme-preview.png diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee index 4f76d8ce486..777c62dc1b7 100644 --- a/app/assets/javascripts/activities.js.coffee +++ b/app/assets/javascripts/activities.js.coffee @@ -12,7 +12,7 @@ class @Activities toggleFilter: (sender) -> - sender.parent().toggleClass "inactive" + sender.parent().toggleClass "active" event_filters = $.cookie("event_filter") filter = sender.attr("id").split("_")[0] if event_filters diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index fafa5cdfaa4..9e5d594c861 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -1,61 +1,24 @@ @Api = - users_path: "/api/:version/users.json" - user_path: "/api/:version/users/:id.json" - notes_path: "/api/:version/projects/:id/notes.json" + groups_path: "/api/:version/groups.json" + group_path: "/api/:version/groups/:id.json" namespaces_path: "/api/:version/namespaces.json" - project_users_path: "/api/:version/projects/:id/users.json" - # Get 20 (depends on api) recent notes - # and sort the ascending from oldest to newest - notes: (project_id, callback) -> - url = Api.buildUrl(Api.notes_path) - url = url.replace(':id', project_id) - - $.ajax( - url: url, - data: - private_token: gon.api_token - gfm: true - recent: true - dataType: "json" - ).done (notes) -> - notes.sort (a, b) -> - return a.id - b.id - callback(notes) - - user: (user_id, callback) -> - url = Api.buildUrl(Api.user_path) - url = url.replace(':id', user_id) + group: (group_id, callback) -> + url = Api.buildUrl(Api.group_path) + url = url.replace(':id', group_id) $.ajax( url: url data: private_token: gon.api_token dataType: "json" - ).done (user) -> - callback(user) - - # Return users list. Filtered by query - # Only active users retrieved - users: (query, callback) -> - url = Api.buildUrl(Api.users_path) - - $.ajax( - url: url - data: - private_token: gon.api_token - search: query - per_page: 20 - active: true - dataType: "json" - ).done (users) -> - callback(users) + ).done (group) -> + callback(group) - # Return project users list. Filtered by query - # Only active users retrieved - projectUsers: (project_id, query, callback) -> - url = Api.buildUrl(Api.project_users_path) - url = url.replace(':id', project_id) + # Return groups list. Filtered by query + # Only active groups retrieved + groups: (query, skip_ldap, callback) -> + url = Api.buildUrl(Api.groups_path) $.ajax( url: url @@ -63,10 +26,9 @@ private_token: gon.api_token search: query per_page: 20 - active: true dataType: "json" - ).done (users) -> - callback(users) + ).done (groups) -> + callback(groups) # Return namespaces list. Filtered by query namespaces: (query, callback) -> diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index e9a28c12159..c18ea929506 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -16,16 +16,16 @@ #= require jquery.scrollTo #= require jquery.blockUI #= require jquery.turbolinks +#= require jquery.sticky-kit.min #= require turbolinks +#= require autosave #= require bootstrap -#= require password_strength #= require select2 #= require raphael #= require g.raphael-min #= require g.bar-min #= require chart-lib.min #= require branch-graph -#= require highlight.pack #= require ace/ace #= require ace/ext-searchbox #= require d3 @@ -33,14 +33,14 @@ #= require nprogress #= require nprogress-turbolinks #= require dropzone -#= require semantic-ui/sidebar #= require mousetrap #= require mousetrap/pause #= require shortcuts #= require shortcuts_navigation #= require shortcuts_dashboard_navigation -#= require shortcuts_issueable +#= require shortcuts_issuable #= require shortcuts_network +#= require cal-heatmap #= require_tree . window.slugify = (text) -> @@ -49,14 +49,6 @@ window.slugify = (text) -> window.ajaxGet = (url) -> $.ajax({type: "GET", url: url, dataType: "script"}) -window.showAndHide = (selector) -> - -window.errorMessage = (message) -> - ehtml = $("<p>") - ehtml.addClass("error_message") - ehtml.html(message) - ehtml - window.split = (val) -> return val.split( /,\s*/ ) @@ -82,44 +74,42 @@ window.disableButtonIfEmptyField = (field_selector, button_selector) -> # Disable button if any input field with given selector is empty window.disableButtonIfAnyEmptyField = (form, form_selector, button_selector) -> closest_submit = form.find(button_selector) - empty = false - form.find('input').filter(form_selector).each -> - empty = true if rstrip($(this).val()) is "" - - if empty - closest_submit.disable() - else - closest_submit.enable() - - form.keyup -> - empty = false + updateButtons = -> + filled = true form.find('input').filter(form_selector).each -> - empty = true if rstrip($(this).val()) is "" + filled = rstrip($(this).val()) != "" || !$(this).attr('required') - if empty - closest_submit.disable() - else + if filled closest_submit.enable() + else + closest_submit.disable() + + updateButtons() + form.keyup(updateButtons) window.sanitize = (str) -> return str.replace(/<(?:.|\n)*?>/gm, '') -window.linkify = (str) -> - exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig - return str.replace(exp,"<a href='$1'>$1</a>") - -window.simpleFormat = (str) -> - linkify(sanitize(str).replace(/\n/g, '<br />')) - window.unbindEvents = -> - $(document).unbind('scroll') $(document).off('scroll') +window.shiftWindow = -> + scrollBy 0, -50 + document.addEventListener("page:fetch", unbindEvents) +# Scroll the window to avoid the topnav bar +# https://github.com/twitter/bootstrap/issues/1768 +if location.hash + setTimeout shiftWindow, 1 +window.addEventListener "hashchange", shiftWindow + $ -> - # Click a .one_click_select field, select the contents - $(".one_click_select").on 'click', -> $(@).select() + # Click a .js-select-on-focus field, select the contents + $(".js-select-on-focus").on "focusin", -> + # Prevent a mouseup event from deselecting the input + $(this).select().one 'mouseup', (e) -> + e.preventDefault() $('.remove-row').bind 'ajax:success', -> $(this).closest('li').fadeOut() @@ -135,17 +125,23 @@ $ -> ), 1 # Initialize tooltips - $('.has_tooltip').tooltip() - - # Bottom tooltip - $('.has_bottom_tooltip').tooltip(placement: 'bottom') + $('body').tooltip({ + selector: '.has_tooltip, [data-toggle="tooltip"], .page-sidebar-collapsed .nav-sidebar a' + placement: (_, el) -> + $el = $(el) + if $el.attr('id') == 'js-shortcuts-home' + # Place the logo tooltip on the right when collapsed, bottom when expanded + $el.parents('header').hasClass('header-collapsed') and 'right' or 'bottom' + else + # Otherwise use the data-placement attribute, or 'bottom' if undefined + $el.data('placement') or 'bottom' + }) # Form submitter $('.trigger-submit').on 'change', -> $(@).parents('form').submit() - $("abbr.timeago").timeago() - $('.js-timeago').timeago() + $('abbr.timeago, .js-timeago').timeago() # Flash if (flash = $(".flash-container")).length > 0 @@ -170,14 +166,17 @@ $ -> $(@).next('table').show() $(@).remove() + $('.navbar-toggle').on 'click', -> + $('.header-content .title').toggle() + $('.header-content .navbar-collapse').toggle() + # Show/hide comments on diff $("body").on "click", ".js-toggle-diff-comments", (e) -> - $(@).find('i'). - toggleClass('fa fa-chevron-down'). - toggleClass('fa fa-chevron-up') + $(@).toggleClass('active') $(@).closest(".diff-file").find(".notes_holder").toggle() e.preventDefault() + $(document).off "click", '.js-confirm-danger' $(document).on "click", '.js-confirm-danger', (e) -> e.preventDefault() btn = $(e.target) @@ -185,13 +184,4 @@ $ -> form = btn.closest("form") new ConfirmDangerModal(form, text) -(($) -> - # Disable an element and add the 'disabled' Bootstrap class - $.fn.extend disable: -> - $(@).attr('disabled', 'disabled').addClass('disabled') - - # Enable an element and remove the 'disabled' Bootstrap class - $.fn.extend enable: -> - $(@).removeAttr('disabled').removeClass('disabled') - -)(jQuery) + new Aside() diff --git a/app/assets/javascripts/aside.js.coffee b/app/assets/javascripts/aside.js.coffee new file mode 100644 index 00000000000..85473101944 --- /dev/null +++ b/app/assets/javascripts/aside.js.coffee @@ -0,0 +1,17 @@ +class @Aside + constructor: -> + $(document).off "click", "a.show-aside" + $(document).on "click", 'a.show-aside', (e) -> + e.preventDefault() + btn = $(e.currentTarget) + icon = btn.find('i') + console.log('1') + + if icon.hasClass('fa-angle-left') + btn.parent().find('section').hide() + btn.parent().find('aside').fadeIn() + icon.removeClass('fa-angle-left').addClass('fa-angle-right') + else + btn.parent().find('aside').hide() + btn.parent().find('section').fadeIn() + icon.removeClass('fa-angle-right').addClass('fa-angle-left') diff --git a/app/assets/javascripts/autosave.js.coffee b/app/assets/javascripts/autosave.js.coffee new file mode 100644 index 00000000000..5d3fe81da74 --- /dev/null +++ b/app/assets/javascripts/autosave.js.coffee @@ -0,0 +1,39 @@ +class @Autosave + constructor: (field, key) -> + @field = field + + key = key.join("/") if key.join? + @key = "autosave/#{key}" + + @field.data "autosave", this + + @restore() + + @field.on "input", => @save() + + restore: -> + return unless window.localStorage? + + try + text = window.localStorage.getItem @key + catch + return + + @field.val text if text?.length > 0 + @field.trigger "input" + + save: -> + return unless window.localStorage? + + text = @field.val() + if text?.length > 0 + try + window.localStorage.setItem @key, text + else + @reset() + + reset: -> + return unless window.localStorage? + + try + window.localStorage.removeItem @key diff --git a/app/assets/javascripts/behaviors/taskable.js.coffee b/app/assets/javascripts/behaviors/taskable.js.coffee deleted file mode 100644 index ddce71c1886..00000000000 --- a/app/assets/javascripts/behaviors/taskable.js.coffee +++ /dev/null @@ -1,21 +0,0 @@ -window.updateTaskState = (taskableType) -> - objType = taskableType.data - isChecked = $(this).prop("checked") - if $(this).is(":checked") - stateEvent = "task_check" - else - stateEvent = "task_uncheck" - - taskableUrl = $("form.edit-" + objType).first().attr("action") - taskableNum = taskableUrl.match(/\d+$/) - taskNum = 0 - $("li.task-list-item input:checkbox").each( (index, e) => - if e == this - taskNum = index + 1 - ) - - $.ajax - type: "PATCH" - url: taskableUrl - data: objType + "[state_event]=" + stateEvent + - "&" + objType + "[task_num]=" + taskNum diff --git a/app/assets/javascripts/behaviors/toggle_diff_line_wrap_behavior.coffee b/app/assets/javascripts/behaviors/toggle_diff_line_wrap_behavior.coffee deleted file mode 100644 index 691ed4f98ae..00000000000 --- a/app/assets/javascripts/behaviors/toggle_diff_line_wrap_behavior.coffee +++ /dev/null @@ -1,14 +0,0 @@ -$ -> - # Toggle line wrapping in diff. - # - # %div.diff-file - # %input.js-toggle-diff-line-wrap - # %td.line_content - # - $("body").on "click", ".js-toggle-diff-line-wrap", (e) -> - diffFile = $(@).closest(".diff-file") - if $(@).is(":checked") - diffFile.addClass("diff-wrap-lines") - else - diffFile.removeClass("diff-wrap-lines") - diff --git a/app/assets/javascripts/blob.js.coffee b/app/assets/javascripts/blob.js.coffee deleted file mode 100644 index a5f15f80c5c..00000000000 --- a/app/assets/javascripts/blob.js.coffee +++ /dev/null @@ -1,73 +0,0 @@ -class @BlobView - constructor: -> - # handle multi-line select - handleMultiSelect = (e) -> - [ first_line, last_line ] = parseSelectedLines() - [ line_number ] = parseSelectedLines($(this).attr("id")) - hash = "L#{line_number}" - - if e.shiftKey and not isNaN(first_line) and not isNaN(line_number) - if line_number < first_line - last_line = first_line - first_line = line_number - else - last_line = line_number - - hash = if first_line == last_line then "L#{first_line}" else "L#{first_line}-#{last_line}" - - setHash(hash) - e.preventDefault() - - # See if there are lines selected - # "#L12" and "#L34-56" supported - highlightBlobLines = (e) -> - [ first_line, last_line ] = parseSelectedLines() - - unless isNaN first_line - $("#tree-content-holder .highlight .line").removeClass("hll") - $("#LC#{line}").addClass("hll") for line in [first_line..last_line] - $.scrollTo("#L#{first_line}") unless e? - - # parse selected lines from hash - # always return first and last line (initialized to NaN) - parseSelectedLines = (str) -> - first_line = NaN - last_line = NaN - hash = str || window.location.hash - - if hash isnt "" - matches = hash.match(/\#?L(\d+)(\-(\d+))?/) - first_line = parseInt(matches?[1]) - last_line = parseInt(matches?[3]) - last_line = first_line if isNaN(last_line) - - [ first_line, last_line ] - - setHash = (hash) -> - hash = hash.replace(/^\#/, "") - nodes = $("#" + hash) - # if any nodes are using this id, they must be temporarily changed - # also, add a temporary div at the top of the screen to prevent scrolling - if nodes.length > 0 - scroll_top = $(document).scrollTop() - nodes.attr("id", "") - tmp = $("<div></div>") - .css({ position: "absolute", visibility: "hidden", top: scroll_top + "px" }) - .attr("id", hash) - .appendTo(document.body) - - window.location.hash = hash - - # restore the nodes - if nodes.length > 0 - tmp.remove() - nodes.attr("id", hash) - - # initialize multi-line select - $("#tree-content-holder .line-numbers a[id^=L]").on("click", handleMultiSelect) - - # Highlight the correct lines on load - highlightBlobLines() - - # Highlight the correct lines when the hash part of the URL changes - $(window).on("hashchange", highlightBlobLines) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee new file mode 100644 index 00000000000..2e91a06daa8 --- /dev/null +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -0,0 +1,44 @@ +class @EditBlob + constructor: (assets_path, mode)-> + ace.config.set "modePath", assets_path + '/ace' + ace.config.loadModule "ace/ext/searchbox" + if mode + ace_mode = mode + editor = ace.edit("editor") + editor.focus() + @editor = editor + + if ace_mode + editor.getSession().setMode "ace/mode/" + ace_mode + + disableButtonIfEmptyField "#commit_message", ".js-commit-button" + $(".js-commit-button").click -> + $("#file-content").val editor.getValue() + $(".file-editor form").submit() + return false + + editModePanes = $(".js-edit-mode-pane") + editModeLinks = $(".js-edit-mode a") + editModeLinks.click (event) -> + event.preventDefault() + currentLink = $(this) + paneId = currentLink.attr("href") + currentPane = editModePanes.filter(paneId) + editModeLinks.parent().removeClass "active hover" + currentLink.parent().addClass "active hover" + editModePanes.hide() + if paneId is "#preview" + currentPane.fadeIn 200 + $.post currentLink.data("preview-url"), + content: editor.getValue() + , (response) -> + currentPane.empty().append response + return + + else + currentPane.fadeIn 200 + editor.focus() + return + + editor: -> + return @editor diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee new file mode 100644 index 00000000000..ab8f98715e8 --- /dev/null +++ b/app/assets/javascripts/blob/new_blob.js.coffee @@ -0,0 +1,21 @@ +class @NewBlob + constructor: (assets_path, mode)-> + ace.config.set "modePath", assets_path + '/ace' + ace.config.loadModule "ace/ext/searchbox" + if mode + ace_mode = mode + editor = ace.edit("editor") + editor.focus() + @editor = editor + + if ace_mode + editor.getSession().setMode "ace/mode/" + ace_mode + + disableButtonIfEmptyField "#commit_message", ".js-commit-button" + $(".js-commit-button").click -> + $("#file-content").val editor.getValue() + $(".file-editor form").submit() + return false + + editor: -> + return @editor diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/branch-graph.js.coffee index 010a2b0e42b..917228bd276 100644 --- a/app/assets/javascripts/branch-graph.js.coffee +++ b/app/assets/javascripts/branch-graph.js.coffee @@ -214,7 +214,7 @@ class @BranchGraph stroke: @colors[commit.space] "stroke-width": 2 ) - r.image(gon.relative_url_root + commit.author.icon, avatar_box_x, avatar_box_y, 20, 20) + r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20) r.text(@offsetX + @unitSpace * @mspace + 35, y, commit.message.split("\n")[0]).attr( "text-anchor": "start" font: "14px Monaco, monospace" diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee new file mode 100644 index 00000000000..4c4bc3d66ed --- /dev/null +++ b/app/assets/javascripts/calendar.js.coffee @@ -0,0 +1,39 @@ +class @Calendar + options = + month: "short" + day: "numeric" + year: "numeric" + + 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() / 80 + 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/confirm_danger_modal.js.coffee b/app/assets/javascripts/confirm_danger_modal.js.coffee index bb99edbd09e..66e34dd4a08 100644 --- a/app/assets/javascripts/confirm_danger_modal.js.coffee +++ b/app/assets/javascripts/confirm_danger_modal.js.coffee @@ -8,11 +8,13 @@ class @ConfirmDangerModal submit = $('.js-confirm-danger-submit') submit.disable() + $('.js-confirm-danger-input').off 'input' $('.js-confirm-danger-input').on 'input', -> if rstrip($(@).val()) is project_path submit.enable() else submit.disable() + $('.js-confirm-danger-submit').off 'click' $('.js-confirm-danger-submit').on 'click', => @form.submit() diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee index 6ef5a539b8f..00ee503ff16 100644 --- a/app/assets/javascripts/dashboard.js.coffee +++ b/app/assets/javascripts/dashboard.js.coffee @@ -1,30 +1,3 @@ class @Dashboard constructor: -> - @initSidebarTab() - - $(".dash-filter").keyup -> - terms = $(this).val() - uiBox = $(this).parents('.panel').first() - if terms == "" || terms == undefined - uiBox.find(".dash-list li").show() - else - uiBox.find(".dash-list li").each (index) -> - name = $(this).find(".filter-title").text() - - if name.toLowerCase().search(terms.toLowerCase()) == -1 - $(this).hide() - else - $(this).show() - - - - initSidebarTab: -> - key = "dashboard_sidebar_filter" - - # store selection in cookie - $('.dash-sidebar-tabs a').on 'click', (e) -> - $.cookie(key, $(e.target).attr('id')) - - # show tab from cookie - sidebar_filter = $.cookie(key) - $("#" + sidebar_filter).tab('show') if sidebar_filter + new ProjectsList() diff --git a/app/assets/javascripts/diff.js.coffee b/app/assets/javascripts/diff.js.coffee index 52b4208524f..069f91c30e1 100644 --- a/app/assets/javascripts/diff.js.coffee +++ b/app/assets/javascripts/diff.js.coffee @@ -1,6 +1,7 @@ class @Diff UNFOLD_COUNT = 20 constructor: -> + $(document).off('click', '.js-unfold') $(document).on('click', '.js-unfold', (event) => target = $(event.target) unfoldBottom = target.hasClass('js-unfold-bottom') diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index e8b71a71945..84873e389ea 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -4,12 +4,10 @@ $ -> class Dispatcher constructor: () -> @initSearch() - @initHighlight() @initPageScripts() initPageScripts: -> page = $('body').attr('data-page') - project_id = $('body').attr('data-project-id') unless page return false @@ -23,51 +21,73 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() when 'projects:issues:show' new Issue() - shortcut_handler = new ShortcutsIssueable() + shortcut_handler = new ShortcutsIssuable() new ZenMode() when 'projects:milestones:show' new Milestone() - when 'projects:milestones:new' + when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() + new DropzoneInput($('.milestone-form')) + when 'projects:compare:show' + new Diff() when 'projects:issues:new','projects:issues:edit' GitLab.GfmAutoComplete.setup() shortcut_handler = new ShortcutsNavigation() new ZenMode() + new DropzoneInput($('.issue-form')) + if page == 'projects:issues:new' + new IssuableForm($('.issue-form')) when 'projects:merge_requests:new', 'projects:merge_requests:edit' GitLab.GfmAutoComplete.setup() new Diff() shortcut_handler = new ShortcutsNavigation() new ZenMode() + new DropzoneInput($('.merge-request-form')) + if page == 'projects:merge_requests:new' + new IssuableForm($('.merge-request-form')) when 'projects:merge_requests:show' new Diff() - shortcut_handler = new ShortcutsIssueable() + shortcut_handler = new ShortcutsIssuable() new ZenMode() when "projects:merge_requests:diffs" new Diff() + new ZenMode() when 'projects:merge_requests:index' shortcut_handler = new ShortcutsNavigation() - when 'dashboard:show' + MergeRequests.init() + when 'dashboard:show', 'root:show' new Dashboard() new Activities() + when 'dashboard:projects:starred' + new Activities() + new ProjectsList() when 'projects:commit:show' new Commit() new Diff() + new ZenMode() shortcut_handler = new ShortcutsNavigation() when 'projects:commits:show' shortcut_handler = new ShortcutsNavigation() - when 'groups:show', 'projects:show' + when 'projects:show' new Activities() shortcut_handler = new ShortcutsNavigation() - when 'groups:members' + when 'groups:show' + new Activities() + shortcut_handler = new ShortcutsNavigation() + new ProjectsList() + when 'groups:group_members:index' new GroupMembers() new UsersSelect() + when 'projects:project_members:index' + new ProjectMembers() + new UsersSelect() when 'groups:new', 'groups:edit', 'admin:groups:edit' new GroupAvatar() when 'projects:tree:show' new TreeView() shortcut_handler = new ShortcutsNavigation() when 'projects:blob:show' - new BlobView() + new LineHighlighter() shortcut_handler = new ShortcutsNavigation() when 'projects:labels:new', 'projects:labels:edit' new Labels() @@ -79,6 +99,9 @@ class Dispatcher new ProjectFork() when 'users:show' new User() + new Activities() + when 'admin:users:show' + new ProjectsList() switch path.first() when 'admin' @@ -90,11 +113,21 @@ class Dispatcher new NamespaceSelect() when 'dashboard' shortcut_handler = new ShortcutsDashboardNavigation() + switch path[1] + when 'issues', 'merge_requests' + new UsersSelect() + when 'groups' + switch path[1] + when 'issues', 'merge_requests' + new UsersSelect() when 'profiles' new Profile() when 'projects' new Project() + new ProjectAvatar() switch path[1] + when 'compare' + shortcut_handler = new ShortcutsNavigation() when 'edit' shortcut_handler = new ShortcutsNavigation() new ProjectNew() @@ -103,16 +136,16 @@ class Dispatcher when 'show' new ProjectShow() when 'issues', 'merge_requests' - new ProjectUsersSelect() + new UsersSelect() when 'wikis' new Wikis() shortcut_handler = new ShortcutsNavigation() new ZenMode() + new DropzoneInput($('.wiki-form')) when 'snippets', 'labels', 'graphs' shortcut_handler = new ShortcutsNavigation() - when 'team_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' + when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' shortcut_handler = new ShortcutsNavigation() - new UsersSelect() # If we haven't installed a custom shortcut handler, install the default one @@ -126,10 +159,3 @@ class Dispatcher project_ref = opts.data('autocomplete-project-ref') new SearchAutocomplete(path, project_id, project_ref) - - initHighlight: -> - $('.highlight pre code').each (i, e) -> - $(e).html($.map($(e).html().split("\n"), (line, i) -> - "<span class='line' id='LC" + (i + 1) + "'>" + line + "</span>" - ).join("\n")) - hljs.highlightBlock(e) diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee new file mode 100644 index 00000000000..a7476146010 --- /dev/null +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -0,0 +1,273 @@ +class @DropzoneInput + constructor: (form) -> + Dropzone.autoDiscover = false + alertClass = "alert alert-danger alert-dismissable div-dropzone-alert" + alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"" + divHover = "<div class=\"div-dropzone-hover\"></div>" + divSpinner = "<div class=\"div-dropzone-spinner\"></div>" + divAlert = "<div class=\"" + alertClass + "\"></div>" + iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>" + iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>" + btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>" + project_uploads_path = window.project_uploads_path or null + markdown_preview_path = window.markdown_preview_path or null + max_file_size = gon.max_file_size or 10 + + form_textarea = $(form).find("textarea.markdown-area") + form_textarea.wrap "<div class=\"div-dropzone\"></div>" + form_textarea.on 'paste', (event) => + handlePaste(event) + form_textarea.on "input", -> + hideReferencedUsers() + form_textarea.on "blur", -> + renderMarkdown() + + form_dropzone = $(form).find('.div-dropzone') + form_dropzone.parent().addClass "div-dropzone-wrapper" + form_dropzone.append divHover + $(".div-dropzone-hover").append iconPaperclip + form_dropzone.append divSpinner + $(".div-dropzone-spinner").append iconSpinner + $(".div-dropzone-spinner").css + "opacity": 0 + "display": "none" + + # Preview button + $(document).off "click", ".js-md-preview-button" + $(document).on "click", ".js-md-preview-button", (e) -> + ### + Shows the Markdown preview. + + Lets the server render GFM into Html and displays it. + ### + e.preventDefault() + form = $(this).closest("form") + # toggle tabs + form.find(".js-md-write-button").parent().removeClass "active" + form.find(".js-md-preview-button").parent().addClass "active" + + # toggle content + form.find(".md-write-holder").hide() + form.find(".md-preview-holder").show() + + renderMarkdown() + + # Write button + $(document).off "click", ".js-md-write-button" + $(document).on "click", ".js-md-write-button", (e) -> + ### + Shows the Markdown textarea. + ### + e.preventDefault() + form = $(this).closest("form") + # toggle tabs + form.find(".js-md-write-button").parent().addClass "active" + form.find(".js-md-preview-button").parent().removeClass "active" + + # toggle content + form.find(".md-write-holder").show() + form.find(".md-preview-holder").hide() + + dropzone = form_dropzone.dropzone( + url: project_uploads_path + dictDefaultMessage: "" + clickable: true + paramName: "file" + maxFilesize: max_file_size + uploadMultiple: false + headers: + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + + previewContainer: false + + processing: -> + $(".div-dropzone-alert").alert "close" + + dragover: -> + form_textarea.addClass "div-dropzone-focus" + form.find(".div-dropzone-hover").css "opacity", 0.7 + return + + dragleave: -> + form_textarea.removeClass "div-dropzone-focus" + form.find(".div-dropzone-hover").css "opacity", 0 + return + + drop: -> + form_textarea.removeClass "div-dropzone-focus" + form.find(".div-dropzone-hover").css "opacity", 0 + form_textarea.focus() + return + + success: (header, response) -> + child = $(dropzone[0]).children("textarea") + $(child).val $(child).val() + formatLink(response.link) + "\n" + return + + error: (temp, errorMessage) -> + errorAlert = $(form).find('.error-alert') + checkIfMsgExists = errorAlert.children().length + if checkIfMsgExists is 0 + errorAlert.append divAlert + $(".div-dropzone-alert").append btnAlert + errorMessage + return + + sending: -> + form_dropzone.find(".div-dropzone-spinner").css + "opacity": 0.7 + "display": "inherit" + return + + complete: -> + $(".dz-preview").remove() + $(".markdown-area").trigger "input" + $(".div-dropzone-spinner").css + "opacity": 0 + "display": "none" + return + ) + + child = $(dropzone[0]).children("textarea") + + hideReferencedUsers = -> + referencedUsers = form.find(".referenced-users") + referencedUsers.hide() + + renderReferencedUsers = (users) -> + referencedUsers = form.find(".referenced-users") + + if referencedUsers.length + if users.length >= 10 + referencedUsers.show() + referencedUsers.find(".js-referenced-users-count").text users.length + else + referencedUsers.hide() + + renderMarkdown = -> + preview = form.find(".js-md-preview") + mdText = form.find(".markdown-area").val() + if mdText.trim().length is 0 + preview.text "Nothing to preview." + hideReferencedUsers() + else + preview.text "Loading..." + $.ajax( + type: "POST", + url: markdown_preview_path, + data: { + text: mdText + }, + dataType: "json" + ).success (data) -> + preview.html data.body + + renderReferencedUsers data.references.users + + formatLink = (link) -> + text = "[#{link.alt}](#{link.url})" + text = "!#{text}" if link.is_image + text + + handlePaste = (event) -> + pasteEvent = event.originalEvent + if pasteEvent.clipboardData and pasteEvent.clipboardData.items + image = isImage(pasteEvent) + if image + event.preventDefault() + + filename = getFilename(pasteEvent) or "image.png" + text = "{{" + filename + "}}" + pasteText(text) + uploadFile image.getAsFile(), filename + + isImage = (data) -> + i = 0 + while i < data.clipboardData.items.length + item = data.clipboardData.items[i] + if item.type.indexOf("image") isnt -1 + return item + i++ + return false + + pasteText = (text) -> + caretStart = $(child)[0].selectionStart + caretEnd = $(child)[0].selectionEnd + textEnd = $(child).val().length + + beforeSelection = $(child).val().substring 0, caretStart + afterSelection = $(child).val().substring caretEnd, textEnd + $(child).val beforeSelection + text + afterSelection + form_textarea.trigger "input" + + getFilename = (e) -> + if window.clipboardData and window.clipboardData.getData + value = window.clipboardData.getData("Text") + else if e.clipboardData and e.clipboardData.getData + value = e.clipboardData.getData("text/plain") + + value = value.split("\r") + value.first() + + uploadFile = (item, filename) -> + formData = new FormData() + formData.append "file", item, filename + $.ajax + url: project_uploads_path + type: "POST" + data: formData + dataType: "json" + processData: false + contentType: false + headers: + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + + beforeSend: -> + showSpinner() + closeAlertMessage() + + success: (e, textStatus, response) -> + insertToTextArea(filename, formatLink(response.responseJSON.link)) + + error: (response) -> + showError(response.responseJSON.message) + + complete: -> + closeSpinner() + + insertToTextArea = (filename, url) -> + $(child).val (index, val) -> + val.replace("{{" + filename + "}}", url + "\n") + + appendToTextArea = (url) -> + $(child).val (index, val) -> + val + url + "\n" + + showSpinner = (e) -> + form.find(".div-dropzone-spinner").css + "opacity": 0.7 + "display": "inherit" + + closeSpinner = -> + form.find(".div-dropzone-spinner").css + "opacity": 0 + "display": "none" + + showError = (message) -> + errorAlert = $(form).find('.error-alert') + checkIfMsgExists = errorAlert.children().length + if checkIfMsgExists is 0 + errorAlert.append divAlert + $(".div-dropzone-alert").append btnAlert + message + + closeAlertMessage = -> + form.find(".div-dropzone-alert").alert "close" + + form.find(".markdown-selector").click (e) -> + e.preventDefault() + $(@).closest('.gfm-form').find('.div-dropzone').click() + return + + formatLink: (link) -> + text = "[#{link.alt}](#{link.url})" + text = "!#{text}" if link.is_image + text diff --git a/app/assets/javascripts/extensions/jquery.js.coffee b/app/assets/javascripts/extensions/jquery.js.coffee index 40fb6cb9fc3..0a9db8eb5ef 100644 --- a/app/assets/javascripts/extensions/jquery.js.coffee +++ b/app/assets/javascripts/extensions/jquery.js.coffee @@ -1,13 +1,11 @@ -$.fn.showAndHide = -> - $(@).show(). - delay(3000). - fadeOut() - -$.fn.enableButton = -> - $(@).removeAttr('disabled'). - removeClass('disabled') - -$.fn.disableButton = -> - $(@).attr('disabled', 'disabled'). - addClass('disabled') +# Disable an element and add the 'disabled' Bootstrap class +$.fn.extend disable: -> + $(@) + .attr('disabled', 'disabled') + .addClass('disabled') +# Enable an element and remove the 'disabled' Bootstrap class +$.fn.extend enable: -> + $(@) + .removeAttr('disabled') + .removeClass('disabled') diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 00d56ae5b4b..7967892f856 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -2,19 +2,19 @@ window.GitLab ?= {} GitLab.GfmAutoComplete = - # private_token: '' dataSource: '' + # Emoji Emoji: - template: '<li data-value="${insert}">${name} <img alt="${name}" height="20" src="${image}" width="20" /></li>' + template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>' # Team Members Members: - template: '<li data-value="${username}">${username} <small>${name}</small></li>' + template: '<li>${username} <small>${title}</small></li>' # Issues and MergeRequests Issues: - template: '<li data-value="${id}"><small>${id}</small> ${title} </li>' + template: '<li><small>${id}</small> ${title}</li>' # Add GFM auto-completion to all input fields, that accept GFM input. setup: -> @@ -23,45 +23,58 @@ GitLab.GfmAutoComplete = # Emoji input.atwho at: ':' - tpl: @Emoji.template - callbacks: - before_save: (emojis) => - $.map emojis, (em) => name: em.name, insert: em.name+ ':', image: em.path + displayTpl: @Emoji.template + insertTpl: ':${name}:' # Team Members input.atwho at: '@' - tpl: @Members.template - search_key: 'search' + displayTpl: @Members.template + insertTpl: '${atwho-at}${username}' + searchKey: 'search' callbacks: - before_save: (members) => - $.map members, (m) => name: m.name, username: m.username, search: "#{m.username} #{m.name}" + beforeSave: (members) -> + $.map members, (m) -> + title = m.name + title += " (#{m.count})" if m.count + + username: m.username + title: sanitize(title) + search: sanitize("#{m.username} #{m.name}") input.atwho at: '#' alias: 'issues' - search_key: 'search' - tpl: @Issues.template + searchKey: 'search' + displayTpl: @Issues.template + insertTpl: '${atwho-at}${id}' callbacks: - before_save: (issues) -> - $.map issues, (i) -> id: i.iid, title: sanitize(i.title), search: "#{i.iid} #{i.title}" + beforeSave: (issues) -> + $.map issues, (i) -> + id: i.iid + title: sanitize(i.title) + search: "#{i.iid} #{i.title}" input.atwho at: '!' alias: 'mergerequests' - search_key: 'search' - tpl: @Issues.template + searchKey: 'search' + displayTpl: @Issues.template + insertTpl: '${atwho-at}${id}' callbacks: - before_save: (merges) -> - $.map merges, (m) -> id: m.iid, title: sanitize(m.title), search: "#{m.iid} #{m.title}" + beforeSave: (merges) -> + $.map merges, (m) -> + id: m.iid + title: sanitize(m.title) + search: "#{m.iid} #{m.title}" - input.one "focus", => + input.one 'focus', => $.getJSON(@dataSource).done (data) -> # load members - input.atwho 'load', "@", data.members + input.atwho 'load', '@', data.members # load issues - input.atwho 'load', "issues", data.issues + input.atwho 'load', 'issues', data.issues # load merge requests - input.atwho 'load', "mergerequests", data.mergerequests + input.atwho 'load', 'mergerequests', data.mergerequests # load emojis - input.atwho 'load', ":", data.emojis + input.atwho 'load', ':', data.emojis diff --git a/app/assets/javascripts/groups_select.js.coffee b/app/assets/javascripts/groups_select.js.coffee new file mode 100644 index 00000000000..1084e2a17d1 --- /dev/null +++ b/app/assets/javascripts/groups_select.js.coffee @@ -0,0 +1,41 @@ +class @GroupsSelect + constructor: -> + $('.ajax-groups-select').each (i, select) => + skip_ldap = $(select).hasClass('skip_ldap') + + $(select).select2 + placeholder: "Search for a group" + multiple: $(select).hasClass('multiselect') + minimumInputLength: 0 + query: (query) -> + Api.groups query.term, skip_ldap, (groups) -> + data = { results: groups } + query.callback(data) + + initSelection: (element, callback) -> + id = $(element).val() + if id isnt "" + Api.group(id, callback) + + + formatResult: (args...) => + @formatResult(args...) + formatSelection: (args...) => + @formatSelection(args...) + dropdownCssClass: "ajax-groups-dropdown" + escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results + m + + formatResult: (group) -> + if group.avatar_url + avatar = group.avatar_url + else + avatar = gon.default_avatar_url + + "<div class='group-result'> + <div class='group-name'>#{group.name}</div> + <div class='group-path'>#{group.path}</div> + </div>" + + formatSelection: (group) -> + group.name diff --git a/app/assets/javascripts/importer_status.js.coffee b/app/assets/javascripts/importer_status.js.coffee new file mode 100644 index 00000000000..be8d225e73b --- /dev/null +++ b/app/assets/javascripts/importer_status.js.coffee @@ -0,0 +1,35 @@ +class @ImporterStatus + constructor: (@jobs_url, @import_url) -> + this.initStatusPage() + this.setAutoUpdate() + + initStatusPage: -> + $(".js-add-to-import").click (event) => + new_namespace = null + tr = $(event.currentTarget).closest("tr") + id = tr.attr("id").replace("repo_", "") + if tr.find(".import-target input").length > 0 + new_namespace = tr.find(".import-target input").prop("value") + tr.find(".import-target").empty().append(new_namespace + "/" + tr.find(".import-target").data("project_name")) + $.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script' + + $(".js-import-all").click (event) => + $(".js-add-to-import").each -> + $(this).click() + + setAutoUpdate: -> + setInterval (=> + $.get @jobs_url, (data) => + $.each data, (i, job) => + job_item = $("#project_" + job.id) + status_field = job_item.find(".job-status") + + if job.import_status == 'finished' + job_item.removeClass("active").addClass("success") + status_field.html('<span><i class="fa fa-check"></i> done</span>') + else if job.import_status == 'started' + status_field.html("<i class='fa fa-spinner fa-spin'></i> started") + else + status_field.html(job.import_status) + + ), 4000 diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee new file mode 100644 index 00000000000..abd58bcf978 --- /dev/null +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -0,0 +1,28 @@ +class @IssuableForm + constructor: (@form) -> + @titleField = @form.find("input[name*='[title]']") + @descriptionField = @form.find("textarea[name*='[description]']") + + return unless @titleField.length && @descriptionField.length + + @initAutosave() + + @form.on "submit", @resetAutosave + @form.on "click", ".btn-cancel", @resetAutosave + + initAutosave: -> + new Autosave @titleField, [ + document.location.pathname, + document.location.search, + "title" + ] + + new Autosave @descriptionField, [ + document.location.pathname, + document.location.search, + "description" + ] + + resetAutosave: => + @titleField.data("autosave").reset() + @descriptionField.data("autosave").reset() diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 597b4695a6d..74d6b80be5e 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -1,17 +1,46 @@ +#= require jquery.waitforimages +#= require task_list + class @Issue constructor: -> $('.edit-issue.inline-update input[type="submit"]').hide() - $(".issue-box .inline-update").on "change", "select", -> + $(".context .inline-update").on "change", "select", -> $(this).submit() - $(".issue-box .inline-update").on "change", "#issue_assignee_id", -> + $(".context .inline-update").on "change", "#issue_assignee_id", -> $(this).submit() + # Prevent duplicate event bindings + @disableTaskList() + if $("a.btn-close").length - $("li.task-list-item input:checkbox").prop("disabled", false) - - $(".task-list-item input:checkbox").on( - "click" - null - "issue" - updateTaskState - ) + @initTaskList() + + $('.issue-details').waitForImages -> + $('.issuable-affix').affix offset: + top: -> + @top = ($('.issuable-affix').offset().top - 70) + bottom: -> + @bottom = $('.footer').outerHeight(true) + $('.issuable-affix').on 'affix.bs.affix', -> + $(@).width($(@).outerWidth()) + .on 'affixed-top.bs.affix affixed-bottom.bs.affix', -> + $(@).width('') + + initTaskList: -> + $('.issue-details .js-task-list-container').taskList('enable') + $(document).on 'tasklist:changed', '.issue-details .js-task-list-container', @updateTaskList + + disableTaskList: -> + $('.issue-details .js-task-list-container').taskList('disable') + $(document).off 'tasklist:changed', '.issue-details .js-task-list-container' + + # TODO (rspeicher): Make the issue description inline-editable like a note so + # that we can re-use its form here + updateTaskList: -> + patchData = {} + patchData['issue'] = {'description': $('.js-task-list-field', this).val()} + + $.ajax + type: 'PATCH' + url: $('form.js-issue-update').attr('action') + data: patchData diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 2499ad5ad80..40bb9e9cb0c 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -15,7 +15,7 @@ $(this).html totalIssues + 1 else $(this).html totalIssues - 1 - $("body").on "click", ".issues-filters .dropdown-menu a", -> + $("body").on "click", ".issues-other-filters .dropdown-menu a", -> $('.issues-list').block( message: null, overlayCSS: @@ -47,7 +47,7 @@ initSearch: -> @timer = null $("#issue_search").keyup -> - clearTimeout(@timer); + clearTimeout(@timer) @timer = setTimeout(Issues.filterResults, 500) filterResults: => @@ -77,9 +77,9 @@ ids.push $(value).attr("data-id") $("#update_issues_ids").val ids - $(".issues-filters").hide() + $(".issues-other-filters").hide() $(".issues_bulk_update").show() else $("#update_issues_ids").val [] $(".issues_bulk_update").hide() - $(".issues-filters").show() + $(".issues-other-filters").show() diff --git a/app/assets/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee new file mode 100644 index 00000000000..a8b3c1fa33e --- /dev/null +++ b/app/assets/javascripts/line_highlighter.js.coffee @@ -0,0 +1,148 @@ +# LineHighlighter +# +# Handles single- and multi-line selection and highlight for blob views. +# +#= require jquery.scrollTo +# +# ### Example Markup +# +# <div id="tree-content-holder"> +# <div class="file-content"> +# <div class="line-numbers"> +# <a href="#L1" id="L1" data-line-number="1">1</a> +# <a href="#L2" id="L2" data-line-number="2">2</a> +# <a href="#L3" id="L3" data-line-number="3">3</a> +# <a href="#L4" id="L4" data-line-number="4">4</a> +# <a href="#L5" id="L5" data-line-number="5">5</a> +# </div> +# <pre class="code highlight"> +# <code> +# <span id="LC1" class="line">...</span> +# <span id="LC2" class="line">...</span> +# <span id="LC3" class="line">...</span> +# <span id="LC4" class="line">...</span> +# <span id="LC5" class="line">...</span> +# </code> +# </pre> +# </div> +# </div> +# +class @LineHighlighter + # CSS class applied to highlighted lines + highlightClass: 'hll' + + # Internal copy of location.hash so we're not dependent on `location` in tests + _hash: '' + + # Initialize a LineHighlighter object + # + # hash - String URL hash for dependency injection in tests + constructor: (hash = location.hash) -> + @_hash = hash + + @bindEvents() + + unless hash == '' + range = @hashToRange(hash) + + if range[0] + @highlightRange(range) + + # Scroll to the first highlighted line on initial load + # Offset -50 for the sticky top bar, and another -100 for some context + $.scrollTo("#L#{range[0]}", offset: -150) + + bindEvents: -> + $('#tree-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler + + # While it may seem odd to bind to the mousedown event and then throw away + # the click event, there is a method to our madness. + # + # If not done this way, the line number anchor will sometimes keep its + # active state even when the event is cancelled, resulting in an ugly border + # around the link and/or a persisted underline text decoration. + + $('#tree-content-holder').on 'click', 'a[data-line-number]', (event) -> + event.preventDefault() + + clickHandler: (event) => + event.preventDefault() + + @clearHighlight() + + lineNumber = $(event.target).data('line-number') + current = @hashToRange(@_hash) + + unless current[0] && event.shiftKey + # If there's no current selection, or there is but Shift wasn't held, + # treat this like a single-line selection. + @setHash(lineNumber) + @highlightLine(lineNumber) + else if event.shiftKey + if lineNumber < current[0] + range = [lineNumber, current[0]] + else + range = [current[0], lineNumber] + + @setHash(range[0], range[1]) + @highlightRange(range) + + # Unhighlight previously highlighted lines + clearHighlight: -> + $(".#{@highlightClass}").removeClass(@highlightClass) + + # Convert a URL hash String into line numbers + # + # hash - Hash String + # + # Examples: + # + # hashToRange('#L5') # => [5, null] + # hashToRange('#L5-15') # => [5, 15] + # hashToRange('#foo') # => [null, null] + # + # Returns an Array + hashToRange: (hash) -> + matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/) + + if matches && matches.length + first = parseInt(matches[1]) + last = if matches[2] then parseInt(matches[2]) else null + + [first, last] + else + [null, null] + + # Highlight a single line + # + # lineNumber - Line number to highlight + highlightLine: (lineNumber) => + $("#LC#{lineNumber}").addClass(@highlightClass) + + # Highlight all lines within a range + # + # range - Array containing the starting and ending line numbers + highlightRange: (range) -> + if range[1] + for lineNumber in [range[0]..range[1]] + @highlightLine(lineNumber) + else + @highlightLine(range[0]) + + # Set the URL hash string + setHash: (firstLineNumber, lastLineNumber) => + if lastLineNumber + hash = "#L#{firstLineNumber}-#{lastLineNumber}" + else + hash = "#L#{firstLineNumber}" + + @_hash = hash + @__setLocationHash__(hash) + + # Make the actual hash change in the browser + # + # This method is stubbed in tests. + __setLocationHash__: (value) -> + # We're using pushState instead of assigning location.hash directly to + # prevent the page from scrolling on the hashchange event + history.pushState({turbolinks: false, url: value}, document.title, value) diff --git a/app/assets/javascripts/markdown_area.js.coffee b/app/assets/javascripts/markdown_area.js.coffee deleted file mode 100644 index a0ebfc98ce6..00000000000 --- a/app/assets/javascripts/markdown_area.js.coffee +++ /dev/null @@ -1,196 +0,0 @@ -formatLink = (str) -> - "" - -$(document).ready -> - alertClass = "alert alert-danger alert-dismissable div-dropzone-alert" - alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"" - divHover = "<div class=\"div-dropzone-hover\"></div>" - divSpinner = "<div class=\"div-dropzone-spinner\"></div>" - divAlert = "<div class=\"" + alertClass + "\"></div>" - iconPicture = "<i class=\"fa fa-picture-o div-dropzone-icon\"></i>" - iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>" - btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>" - project_image_path_upload = window.project_image_path_upload or null - - $("textarea.markdown-area").wrap "<div class=\"div-dropzone\"></div>" - - $(".div-dropzone").parent().addClass "div-dropzone-wrapper" - - $(".div-dropzone").append divHover - $(".div-dropzone-hover").append iconPicture - $(".div-dropzone").append divSpinner - $(".div-dropzone-spinner").append iconSpinner - $(".div-dropzone-spinner").css - "opacity": 0 - "display": "none" - - dropzone = $(".div-dropzone").dropzone( - url: project_image_path_upload - dictDefaultMessage: "" - clickable: true - paramName: "markdown_img" - maxFilesize: 10 - uploadMultiple: false - acceptedFiles: "image/jpg,image/jpeg,image/gif,image/png" - headers: - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - - previewContainer: false - - processing: -> - $(".div-dropzone-alert").alert "close" - - dragover: -> - $(".div-dropzone > textarea").addClass "div-dropzone-focus" - $(".div-dropzone-hover").css "opacity", 0.7 - return - - dragleave: -> - $(".div-dropzone > textarea").removeClass "div-dropzone-focus" - $(".div-dropzone-hover").css "opacity", 0 - return - - drop: -> - $(".div-dropzone > textarea").removeClass "div-dropzone-focus" - $(".div-dropzone-hover").css "opacity", 0 - $(".div-dropzone > textarea").focus() - return - - success: (header, response) -> - child = $(dropzone[0]).children("textarea") - $(child).val $(child).val() + formatLink(response.link) + "\n" - return - - error: (temp, errorMessage) -> - checkIfMsgExists = $(".error-alert").children().length - if checkIfMsgExists is 0 - $(".error-alert").append divAlert - $(".div-dropzone-alert").append btnAlert + errorMessage - return - - sending: -> - $(".div-dropzone-spinner").css - "opacity": 0.7 - "display": "inherit" - return - - complete: -> - $(".dz-preview").remove() - $(".markdown-area").trigger "input" - $(".div-dropzone-spinner").css - "opacity": 0 - "display": "none" - return - ) - - child = $(dropzone[0]).children("textarea") - - formatLink = (str) -> - "" - - handlePaste = (e) -> - e.preventDefault() - my_event = e.originalEvent - - if my_event.clipboardData and my_event.clipboardData.items - processItem(my_event) - - processItem = (e) -> - image = isImage(e) - if image - filename = getFilename(e) or "image.png" - text = "{{" + filename + "}}" - pasteText(text) - uploadFile image.getAsFile(), filename - - else - text = e.clipboardData.getData("text/plain") - pasteText(text) - - isImage = (data) -> - i = 0 - while i < data.clipboardData.items.length - item = data.clipboardData.items[i] - if item.type.indexOf("image") isnt -1 - return item - i++ - return false - - pasteText = (text) -> - caretStart = $(child)[0].selectionStart - caretEnd = $(child)[0].selectionEnd - textEnd = $(child).val().length - - beforeSelection = $(child).val().substring 0, caretStart - afterSelection = $(child).val().substring caretEnd, textEnd - $(child).val beforeSelection + text + afterSelection - $(".markdown-area").trigger "input" - - getFilename = (e) -> - if window.clipboardData and window.clipboardData.getData - value = window.clipboardData.getData("Text") - else if e.clipboardData and e.clipboardData.getData - value = e.clipboardData.getData("text/plain") - - value = value.split("\r") - value.first() - - uploadFile = (item, filename) -> - formData = new FormData() - formData.append "markdown_img", item, filename - $.ajax - url: project_image_path_upload - type: "POST" - data: formData - dataType: "json" - processData: false - contentType: false - headers: - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - - beforeSend: -> - showSpinner() - closeAlertMessage() - - success: (e, textStatus, response) -> - insertToTextArea(filename, formatLink(response.responseJSON.link)) - - error: (response) -> - showError(response.responseJSON.message) - - complete: -> - closeSpinner() - - insertToTextArea = (filename, url) -> - $(child).val (index, val) -> - val.replace("{{" + filename + "}}", url + "\n") - - appendToTextArea = (url) -> - $(child).val (index, val) -> - val + url + "\n" - - showSpinner = (e) -> - $(".div-dropzone-spinner").css - "opacity": 0.7 - "display": "inherit" - - closeSpinner = -> - $(".div-dropzone-spinner").css - "opacity": 0 - "display": "none" - - showError = (message) -> - checkIfMsgExists = $(".error-alert").children().length - if checkIfMsgExists is 0 - $(".error-alert").append divAlert - $(".div-dropzone-alert").append btnAlert + message - - closeAlertMessage = -> - $(".div-dropzone-alert").alert "close" - - $(".markdown-selector").click (e) -> - e.preventDefault() - $(@).closest('.gfm-form').find('.div-dropzone').click() - return - - return diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 46e06424e5a..5c0bc686111 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -1,24 +1,41 @@ +#= require jquery.waitforimages +#= require task_list + +#= require merge_request_tabs + class @MergeRequest + # Initialize MergeRequest behavior + # + # Options: + # action - String, current controller action + # constructor: (@opts) -> @initContextWidget() this.$el = $('.merge-request') - @diffs_loaded = if @opts.action == 'diffs' then true else false - @commits_loaded = false - - this.activateTab(@opts.action) - this.bindEvents() - - this.initMergeWidget() this.$('.show-all-commits').on 'click', => this.showAllCommits() - modal = $('#modal_merge_info').modal(show: false) + # `MergeRequests#new` has no tab-persisting or lazy-loading behavior + unless @opts.action == 'new' + new MergeRequestTabs(@opts) - disableButtonIfEmptyField '#commit_message', '.accept_merge_request' + # Prevent duplicate event bindings + @disableTaskList() if $("a.btn-close").length - $("li.task-list-item input:checkbox").prop("disabled", false) + @initTaskList() + + $('.merge-request-details').waitForImages -> + $('.issuable-affix').affix offset: + top: -> + @top = ($('.issuable-affix').offset().top - 70) + bottom: -> + @bottom = $('.footer').outerHeight(true) + $('.issuable-affix').on 'affix.bs.affix', -> + $(@).width($(@).outerWidth()) + .on 'affixed-top.bs.affix affixed-bottom.bs.affix', -> + $(@).width('') # Local jQuery finder $: (selector) -> @@ -26,109 +43,30 @@ class @MergeRequest initContextWidget: -> $('.edit-merge_request.inline-update input[type="submit"]').hide() - $(".issue-box .inline-update").on "change", "select", -> + $(".context .inline-update").on "change", "select", -> $(this).submit() - $(".issue-box .inline-update").on "change", "#merge_request_assignee_id", -> + $(".context .inline-update").on "change", "#merge_request_assignee_id", -> $(this).submit() - initMergeWidget: -> - this.showState( @opts.current_status ) - - if this.$('.automerge_widget').length and @opts.check_enable - $.get @opts.url_to_automerge_check, (data) => - this.showState( data.merge_status ) - , 'json' - - if @opts.ci_enable - $.get @opts.url_to_ci_check, (data) => - this.showCiState data.status - if data.coverage - this.showCiCoverage data.coverage - , 'json' - - bindEvents: -> - this.$('.merge-request-tabs').on 'click', 'a', (event) => - a = $(event.currentTarget) - - href = a.attr('href') - History.replaceState {path: href}, document.title, href - - event.preventDefault() - - this.$('.merge-request-tabs').on 'click', 'li', (event) => - this.activateTab($(event.currentTarget).data('action')) - - this.$('.accept_merge_request').on 'click', -> - $('.automerge_widget.can_be_merged').hide() - $('.merge-in-progress').show() - - this.$('.remove_source_branch').on 'click', -> - $('.remove_source_branch_widget').hide() - $('.remove_source_branch_in_progress').show() - - this.$(".remove_source_branch").on "ajax:success", (e, data, status, xhr) -> - location.reload() - - this.$(".remove_source_branch").on "ajax:error", (e, data, status, xhr) => - this.$('.remove_source_branch_widget').hide() - this.$('.remove_source_branch_in_progress').hide() - this.$('.remove_source_branch_widget.failed').show() - - $(".task-list-item input:checkbox").on( - "click" - null - "merge_request" - updateTaskState - ) - - activateTab: (action) -> - this.$('.merge-request-tabs li').removeClass 'active' - this.$('.tab-content').hide() - switch action - when 'diffs' - this.$('.merge-request-tabs .diffs-tab').addClass 'active' - this.loadDiff() unless @diffs_loaded - this.$('.diffs').show() - else - this.$('.merge-request-tabs .notes-tab').addClass 'active' - this.$('.notes').show() + showAllCommits: -> + this.$('.first-commits').remove() + this.$('.all-commits').removeClass 'hide' - showState: (state) -> - $('.automerge_widget').hide() - $('.automerge_widget.' + state).show() + initTaskList: -> + $('.merge-request-details .js-task-list-container').taskList('enable') + $(document).on 'tasklist:changed', '.merge-request-details .js-task-list-container', @updateTaskList - showCiState: (state) -> - $('.ci_widget').hide() - allowed_states = ["failed", "running", "pending", "success"] - if state in allowed_states - $('.ci_widget.ci-' + state).show() - else - $('.ci_widget.ci-error').show() + disableTaskList: -> + $('.merge-request-details .js-task-list-container').taskList('disable') + $(document).off 'tasklist:changed', '.merge-request-details .js-task-list-container' - showCiCoverage: (coverage) -> - cov_html = $('<span>') - cov_html.addClass('ci-coverage') - cov_html.text('Coverage ' + coverage + '%') - $('.ci_widget:visible').append(cov_html) + # TODO (rspeicher): Make the merge request description inline-editable like a + # note so that we can re-use its form here + updateTaskList: -> + patchData = {} + patchData['merge_request'] = {'description': $('.js-task-list-field', this).val()} - loadDiff: (event) -> $.ajax - type: 'GET' - url: this.$('.merge-request-tabs .diffs-tab a').attr('href') - beforeSend: => - this.$('.mr-loading-status .loading').show() - complete: => - @diffs_loaded = true - this.$('.mr-loading-status .loading').hide() - success: (data) => - this.$(".diffs").html(data.html) - dataType: 'json' - - showAllCommits: -> - this.$('.first-commits').remove() - this.$('.all-commits').removeClass 'hide' - - alreadyOrCannotBeMerged: -> - this.$('.automerge_widget').hide() - this.$('.merge-in-progress').hide() - this.$('.automerge_widget.already_cannot_be_merged').show() + type: 'PATCH' + url: $('form.js-merge-request-update').attr('action') + data: patchData diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee new file mode 100644 index 00000000000..de9a4c2cc2f --- /dev/null +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -0,0 +1,153 @@ +# MergeRequestTabs +# +# Handles persisting and restoring the current tab selection and lazily-loading +# content on the MergeRequests#show page. +# +# ### Example Markup +# +# <ul class="nav nav-tabs merge-request-tabs"> +# <li class="notes-tab active"> +# <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> +# Discussion +# </a> +# </li> +# <li class="commits-tab"> +# <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits"> +# Commits +# </a> +# </li> +# <li class="diffs-tab"> +# <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs"> +# Diffs +# </a> +# </li> +# </ul> +# +# <div class="tab-content"> +# <div class="notes tab-pane active" id="notes"> +# Notes Content +# </div> +# <div class="commits tab-pane" id="commits"> +# Commits Content +# </div> +# <div class="diffs tab-pane" id="diffs"> +# Diffs Content +# </div> +# </div> +# +# <div class="mr-loading-status"> +# <div class="loading"> +# Loading Animation +# </div> +# </div> +# +class @MergeRequestTabs + diffsLoaded: false + commitsLoaded: false + + constructor: (@opts = {}) -> + # Store the `location` object, allowing for easier stubbing in tests + @_location = location + + @bindEvents() + @activateTab(@opts.action) + + switch @opts.action + when 'commits' then @commitsLoaded = true + when 'diffs' then @diffsLoaded = true + + bindEvents: -> + $(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown + + tabShown: (event) => + $target = $(event.target) + action = $target.data('action') + + if action == 'commits' + @loadCommits($target.attr('href')) + else if action == 'diffs' + @loadDiff($target.attr('href')) + + @setCurrentAction(action) + + # Activate a tab based on the current action + activateTab: (action) -> + action = 'notes' if action == 'show' + $(".merge-request-tabs a[data-action='#{action}']").tab('show') + + # Replaces the current Merge Request-specific action in the URL with a new one + # + # If the action is "notes", the URL is reset to the standard + # `MergeRequests#show` route. + # + # Examples: + # + # location.pathname # => "/namespace/project/merge_requests/1" + # setCurrentAction('diffs') + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # setCurrentAction('notes') + # location.pathname # => "/namespace/project/merge_requests/1" + # + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # setCurrentAction('commits') + # location.pathname # => "/namespace/project/merge_requests/1/commits" + # + # Returns the new URL String + setCurrentAction: (action) => + # Normalize action, just to be safe + action = 'notes' if action == 'show' + + # Remove a trailing '/commits' or '/diffs' + new_state = @_location.pathname.replace(/\/(commits|diffs)\/?$/, '') + + # Append the new action if we're on a tab other than 'notes' + unless action == 'notes' + new_state += "/#{action}" + + # Ensure parameters and hash come along for the ride + new_state += @_location.search + @_location.hash + + # Replace the current history state with the new one without breaking + # Turbolinks' history. + # + # See https://github.com/rails/turbolinks/issues/363 + history.replaceState {turbolinks: true, url: new_state}, document.title, new_state + + new_state + + loadCommits: (source) -> + return if @commitsLoaded + + @_get + url: "#{source}.json" + success: (data) => + document.getElementById('commits').innerHTML = data.html + $('.js-timeago').timeago() + @commitsLoaded = true + + loadDiff: (source) -> + return if @diffsLoaded + + @_get + url: "#{source}.json" + success: (data) => + document.getElementById('diffs').innerHTML = data.html + $('.diff-header').trigger('sticky_kit:recalc') + @diffsLoaded = true + + toggleLoading: -> + $('.mr-loading-status .loading').toggle() + + _get: (options) -> + defaults = { + beforeSend: @toggleLoading + complete: @toggleLoading + dataType: 'json' + type: 'GET' + } + + options = $.extend({}, defaults, options) + + $.ajax(options) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee new file mode 100644 index 00000000000..ca769e06a4e --- /dev/null +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -0,0 +1,58 @@ +class @MergeRequestWidget + # Initialize MergeRequestWidget behavior + # + # check_enable - Boolean, whether to check automerge status + # url_to_automerge_check - String, URL to use to check automerge status + # current_status - String, current automerge status + # ci_enable - Boolean, whether a CI service is enabled + # url_to_ci_check - String, URL to use to check CI status + # + constructor: (@opts) -> + modal = $('#modal_merge_info').modal(show: false) + + mergeInProgress: -> + $.ajax + type: 'GET' + url: $('.merge-request').data('url') + success: (data) => + switch data.state + when 'merged' + location.reload() + else + setTimeout(merge_request_widget.mergeInProgress, 3000) + dataType: 'json' + + getMergeStatus: -> + $.get @opts.url_to_automerge_check, (data) -> + $('.mr-state-widget').replaceWith(data) + + getCiStatus: -> + if @opts.ci_enable + $.get @opts.url_to_ci_check, (data) => + this.showCiState data.status + if data.coverage + this.showCiCoverage data.coverage + , 'json' + + showCiState: (state) -> + $('.ci_widget').hide() + allowed_states = ["failed", "canceled", "running", "pending", "success"] + if state in allowed_states + $('.ci_widget.ci-' + state).show() + switch state + when "failed", "canceled" + @setMergeButtonClass('btn-danger') + when "running", "pending" + @setMergeButtonClass('btn-warning') + else + $('.ci_widget.ci-error').show() + @setMergeButtonClass('btn-danger') + + showCiCoverage: (coverage) -> + cov_html = $('<span>') + cov_html.addClass('ci-coverage') + cov_html.text('Coverage ' + coverage + '%') + $('.ci_widget:visible').append(cov_html) + + setMergeButtonClass: (css_class) -> + $('.accept_merge_request').removeClass("btn-create").addClass(css_class) diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee index 9201c84c5ed..83434c1b9ba 100644 --- a/app/assets/javascripts/merge_requests.js.coffee +++ b/app/assets/javascripts/merge_requests.js.coffee @@ -1,8 +1,35 @@ # # * Filter merge requests # -@merge_requestsPage = -> - $('#assignee_id').select2() - $('#milestone_id').select2() - $('#milestone_id, #assignee_id').on 'change', -> - $(this).closest('form').submit() +@MergeRequests = + init: -> + MergeRequests.initSearch() + + # Make sure we trigger ajax request only after user stop typing + initSearch: -> + @timer = null + $("#issue_search").keyup -> + clearTimeout(@timer) + @timer = setTimeout(MergeRequests.filterResults, 500) + + filterResults: => + form = $("#issue_search_form") + search = $("#issue_search").val() + $('.merge-requests-holder').css("opacity", '0.5') + issues_url = form.attr('action') + '? '+ form.serialize() + + $.ajax + type: "GET" + url: form.attr('action') + data: form.serialize() + complete: -> + $('.merge-requests-holder').css("opacity", '1.0') + success: (data) -> + $('.merge-requests-holder').html(data.html) + # Change url so if user reload a page - search results are saved + History.replaceState {page: issues_url}, document.title, issues_url + MergeRequests.reload() + dataType: "json" + + reload: -> + $('#filter_issue_search').val($('#issue_search').val()) diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee index c42f31933d3..d644d50b669 100644 --- a/app/assets/javascripts/milestone.js.coffee +++ b/app/assets/javascripts/milestone.js.coffee @@ -49,6 +49,13 @@ class @Milestone data: data success: (data) -> if data.saved == true + if data.assignee_avatar_url + img_tag = $('<img/>') + img_tag.attr('src', data.assignee_avatar_url) + img_tag.addClass('avatar s16') + $(li).find('.assignee-icon').html(img_tag) + else + $(li).find('.assignee-icon').html('') $(li).effect 'highlight' else new Flash("Issue update failed", 'alert') diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 978f83dd442..1c05a2b9fe8 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -1,16 +1,25 @@ +#= require autosave +#= require dropzone +#= require dropzone_input +#= require gfm_auto_complete +#= require jquery.atwho +#= require task_list + class @Notes @interval: null - constructor: (notes_url, note_ids, last_fetched_at) -> + constructor: (notes_url, note_ids, last_fetched_at, view) -> @notes_url = notes_url @notes_url = gon.relative_url_root + @notes_url if gon.relative_url_root? @note_ids = note_ids @last_fetched_at = last_fetched_at + @view = view @noteable_url = document.URL @initRefresh() @setupMainTargetNoteForm() @cleanBinding() @addBinding() + @initTaskList() addBinding: -> # add note to UI after creation @@ -36,17 +45,9 @@ class @Notes # delete note attachment $(document).on "click", ".js-note-attachment-delete", @removeAttachment - # Preview button - $(document).on "click", ".js-note-preview-button", @previewNote - - # Preview button - $(document).on "click", ".js-note-write-button", @writeNote - # reset main target form after submit - $(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm - - # attachment button - $(document).on "click", ".js-choose-note-attachment-button", @chooseNoteAttachment + $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton + $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm # update the file name when an attachment is selected $(document).on "change", ".js-note-attachment-input", @updateFormAttachment @@ -63,11 +64,11 @@ class @Notes # fetch notes when tab becomes visible $(document).on "visibilitychange", @visibilityChange - @notes_forms = '.js-main-target-form textarea, .js-discussion-note-form textarea' - $(document).on('keypress', @notes_forms, (e)-> - if e.keyCode == 10 || (e.ctrlKey && e.keyCode == 13) - $(@).parents('form').submit() - ) + # Chrome doesn't fire keypress or keyup for Command+Enter, so we need keydown. + $(document).on 'keydown', '.js-note-text', (e) -> + return if e.originalEvent.repeat + if e.keyCode == 10 || ((e.metaKey || e.ctrlKey) && e.keyCode == 13) + $(@).closest('form').submit() cleanBinding: -> $(document).off "ajax:success", ".js-main-target-form" @@ -77,18 +78,19 @@ class @Notes $(document).off "click", ".note-edit-cancel" $(document).off "click", ".js-note-delete" $(document).off "click", ".js-note-attachment-delete" - $(document).off "click", ".js-note-preview-button" - $(document).off "click", ".js-note-write-button" $(document).off "ajax:complete", ".js-main-target-form" - $(document).off "click", ".js-choose-note-attachment-button" + $(document).off "ajax:success", ".js-main-target-form" $(document).off "click", ".js-discussion-reply-button" $(document).off "click", ".js-add-diff-note-button" $(document).off "visibilitychange" - $(document).off "keypress", @notes_forms + $(document).off "keydown", ".js-note-text" $(document).off "keyup", ".js-note-text" $(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-close" + $('.note .js-task-list-container').taskList('disable') + $(document).off 'tasklist:changed', '.note .js-task-list-container' + initRefresh: -> clearInterval(Notes.interval) Notes.interval = setInterval => @@ -122,10 +124,7 @@ class @Notes if @isNewNote(note) @note_ids.push(note.id) $('ul.main-notes-list').append(note.html) - code = "#note_" + note.id + " .highlight pre code" - $(code).each (i, e) -> - hljs.highlightBlock(e) - + @initTaskList() ### Check if note does not exists on page @@ -133,6 +132,8 @@ class @Notes isNewNote: (note) -> $.inArray(note.id, @note_ids) == -1 + isParallelView: -> + @view == 'parallel' ### Render note in discussion area. @@ -166,47 +167,6 @@ class @Notes @removeDiscussionNoteForm(form) ### - Shows write note textarea. - ### - writeNote: (e) -> - e.preventDefault() - form = $(this).closest("form") - # toggle tabs - form.find(".js-note-write-button").parent().addClass "active" - form.find(".js-note-preview-button").parent().removeClass "active" - - # toggle content - form.find(".note-write-holder").show() - form.find(".note-preview-holder").hide() - - ### - Shows the note preview. - - Lets the server render GFM into Html and displays it. - ### - previewNote: (e) -> - e.preventDefault() - form = $(this).closest("form") - # toggle tabs - form.find(".js-note-write-button").parent().removeClass "active" - form.find(".js-note-preview-button").parent().addClass "active" - - # toggle content - form.find(".note-write-holder").hide() - form.find(".note-preview-holder").show() - - preview = form.find(".js-note-preview") - noteText = form.find(".js-note-text").val() - if noteText.trim().length is 0 - preview.text "Nothing to preview." - else - preview.text "Loading..." - $.post($(this).data("url"), - note: noteText - ).success (previewData) -> - preview.html previewData - - ### Called in response the main target form has been successfully submitted. Removes any errors. @@ -220,17 +180,15 @@ class @Notes form.find(".js-errors").remove() # reset text and preview - form.find(".js-note-write-button").click() + form.find(".js-md-write-button").click() form.find(".js-note-text").val("").trigger "input" - ### - Called when clicking the "Choose File" button. + form.find(".js-note-text").data("autosave").reset() - Opens the file selection dialog. - ### - chooseNoteAttachment: -> - form = $(this).closest("form") - form.find(".js-note-attachment-input").click() + reenableTargetFormSubmitButton: -> + form = $(".js-main-target-form") + + form.find(".js-note-text").trigger "input" ### Shows the main form and does some setup on it. @@ -268,23 +226,34 @@ class @Notes setupNoteForm: (form) -> disableButtonIfEmptyField form.find(".js-note-text"), form.find(".js-comment-button") form.removeClass "js-new-note-form" + form.find('.div-dropzone').remove() # setup preview buttons - form.find(".js-note-write-button, .js-note-preview-button").tooltip placement: "left" - previewButton = form.find(".js-note-preview-button") - form.find(".js-note-text").on "input", -> + form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left" + previewButton = form.find(".js-md-preview-button") + + textarea = form.find(".js-note-text") + + textarea.on "input", -> if $(this).val().trim() isnt "" previewButton.removeClass("turn-off").addClass "turn-on" else previewButton.removeClass("turn-on").addClass "turn-off" + new Autosave textarea, [ + "Note" + form.find("#note_commit_id").val() + form.find("#note_line_code").val() + form.find("#note_noteable_type").val() + form.find("#note_noteable_id").val() + ] # remove notify commit author checkbox for non-commit notes form.find(".js-notify-commit-author").remove() if form.find("#note_noteable_type").val() isnt "Commit" GitLab.GfmAutoComplete.setup() + new DropzoneInput(form) form.show() - ### Called in response to the new note form being submitted @@ -308,11 +277,12 @@ class @Notes Updates the current note field. ### updateNote: (xhr, note, status) => - note_li = $("#note_" + note.id) + note_li = $(".note-row-" + note.id) note_li.replaceWith(note.html) - code = "#note_" + note.id + " .highlight pre code" - $(code).each (i, e) -> - hljs.highlightBlock(e) + note_li.find('.note-edit-form').hide() + note_li.find('.note-body > .note-text').show() + note_li.find('js-task-list-container').taskList('enable') + @enableTaskList() ### Called in response to clicking the edit note link @@ -324,15 +294,31 @@ class @Notes showEditForm: (e) -> e.preventDefault() note = $(this).closest(".note") - note.find(".note-text").hide() + note.find(".note-body > .note-text").hide() + note.find(".note-header").hide() + base_form = note.find(".note-edit-form") + form = base_form.clone().insertAfter(base_form) + form.addClass('current-note-edit-form') + form.find('.div-dropzone').remove() # Show the attachment delete link note.find(".js-note-attachment-delete").show() + + # Setup markdown form GitLab.GfmAutoComplete.setup() - form = note.find(".note-edit-form") + new DropzoneInput(form) + form.show() textarea = form.find("textarea") textarea.focus() + + # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). + # The textarea has the correct value, Chrome just won't show it unless we + # modify it, so let's clear it and re-set it! + value = textarea.val() + textarea.val "" + textarea.val value + disableButtonIfEmptyField textarea, form.find(".js-comment-button") ### @@ -343,9 +329,9 @@ class @Notes cancelEdit: (e) -> e.preventDefault() note = $(this).closest(".note") - note.find(".note-text").show() - note.find(".js-note-attachment-delete").hide() - note.find(".note-edit-form").hide() + note.find(".note-body > .note-text").show() + note.find(".note-header").show() + note.find(".current-note-edit-form").remove() ### Called in response to deleting a note of any kind. @@ -377,7 +363,7 @@ class @Notes removeAttachment: -> note = $(this).closest(".note") note.find(".note-attachment").remove() - note.find(".note-text").show() + note.find(".note-body > .note-text").show() note.find(".js-note-attachment-delete").hide() note.find(".note-edit-form").hide() @@ -408,6 +394,7 @@ class @Notes setupDiscussionNoteForm: (dataHolder, form) => # setup note target form.attr "rel", dataHolder.data("discussionId") + 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") form.find("#note_noteable_type").val dataHolder.data("noteableType") @@ -424,23 +411,44 @@ class @Notes ### addDiffNote: (e) => e.preventDefault() - link = e.target + link = e.currentTarget form = $(".js-new-note-form") row = $(link).closest("tr") nextRow = row.next() - - # does it already have notes? - if nextRow.is(".notes_holder") - replyButton = nextRow.find(".js-discussion-reply-button") - if replyButton.length > 0 - $.proxy(@replyToDiscussionNote, replyButton).call() + hasNotes = nextRow.is(".notes_holder") + addForm = false + targetContent = ".notes_content" + rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>" + + # In parallel view, look inside the correct left/right pane + if @isParallelView() + lineType = $(link).data("lineType") + targetContent += "." + lineType + rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>" + + if hasNotes + notesContent = nextRow.find(targetContent) + if notesContent.length + replyButton = notesContent.find(".js-discussion-reply-button:visible") + if replyButton.length + e.target = replyButton[0] + $.proxy(@replyToDiscussionNote, replyButton[0], e).call() + else + # In parallel view, the form may not be present in one of the panes + noteForm = notesContent.find(".js-discussion-note-form") + if noteForm.length == 0 + addForm = true else # add a notes row and insert the form - row.after "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>" - form.clone().appendTo row.next().find(".notes_content") + row.after rowCssToAdd + addForm = true + + if addForm + newForm = form.clone() + newForm.appendTo row.next().find(targetContent) # show the form - @setupDiscussionNoteForm $(link), row.next().find("form") + @setupDiscussionNoteForm $(link), newForm ### Called in response to "cancel" on a diff note form. @@ -451,6 +459,8 @@ class @Notes removeDiscussionNoteForm: (form)-> row = form.closest("tr") + form.find(".js-note-text").data("autosave").reset() + # show the reply button (will only work for replies) form.prev(".js-discussion-reply-button").show() if row.is(".js-temp-notes-holder") @@ -468,7 +478,7 @@ class @Notes @removeDiscussionNoteForm(form) updateVotes: -> - (new NotesVotes).updateVotes() + true ### Called after an attachment file has been selected. @@ -514,3 +524,13 @@ class @Notes else form.find('.js-note-target-reopen').text('Reopen') form.find('.js-note-target-close').text('Close') + + initTaskList: -> + @enableTaskList() + $(document).on 'tasklist:changed', '.note .js-task-list-container', @updateTaskList + + enableTaskList: -> + $('.note .js-task-list-container').taskList('enable') + + updateTaskList: -> + $('form', this).submit() diff --git a/app/assets/javascripts/notes_votes.js.coffee b/app/assets/javascripts/notes_votes.js.coffee deleted file mode 100644 index 65c149b7886..00000000000 --- a/app/assets/javascripts/notes_votes.js.coffee +++ /dev/null @@ -1,20 +0,0 @@ -class @NotesVotes - updateVotes: -> - votes = $("#votes .votes") - notes = $("#notes-list .note .vote") - - # only update if there is a vote display - if votes.size() - upvotes = notes.filter(".upvote").size() - downvotes = notes.filter(".downvote").size() - votesCount = upvotes + downvotes - upvotesPercent = (if votesCount then (100.0 / votesCount * upvotes) else 0) - downvotesPercent = (if votesCount then (100.0 - upvotesPercent) else 0) - - # change vote bar lengths - votes.find(".bar-success").css "width", upvotesPercent + "%" - votes.find(".bar-danger").css "width", downvotesPercent + "%" - - # replace vote numbers - votes.find(".upvotes").text votes.find(".upvotes").text().replace(/\d+/, upvotes) - votes.find(".downvotes").text votes.find(".downvotes").text().replace(/\d+/, downvotes) diff --git a/app/assets/javascripts/password_strength.js.coffee b/app/assets/javascripts/password_strength.js.coffee deleted file mode 100644 index 825f5630266..00000000000 --- a/app/assets/javascripts/password_strength.js.coffee +++ /dev/null @@ -1,31 +0,0 @@ -#= require pwstrength-bootstrap-1.2.2 -overwritten_messages = - wordSimilarToUsername: "Your password should not contain your username" - -overwritten_rules = - wordSequences: false - -options = - showProgressBar: false - showVerdicts: false - showPopover: true - showErrors: true - showStatus: true - errorMessages: overwritten_messages - -$(document).ready -> - profileOptions = {} - profileOptions.ui = options - profileOptions.rules = - activated: overwritten_rules - - deviseOptions = {} - deviseOptions.common = - usernameField: "#user_username" - deviseOptions.ui = options - deviseOptions.rules = - activated: overwritten_rules - - $("#user_password_profile").pwstrength profileOptions - $("#user_password_sign_up").pwstrength deviseOptions - $("#user_password_recover").pwstrength deviseOptions diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index de356fbec77..bb0b66b86e1 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -1,10 +1,8 @@ class @Profile constructor: -> - $('.edit_user .application-theme input, .edit_user .code-preview-theme input').click -> - # Submit the form - $('.edit_user').submit() - - new Flash("Appearance settings saved", "notice") + # Automatically submit the Preferences form when any of its radio buttons change + $('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> + $(this).parents('form').submit() $('.update-username form').on 'ajax:before', -> $('.loading-gif').show() @@ -12,12 +10,11 @@ class @Profile $(this).find('.update-failed').hide() $('.update-username form').on 'ajax:complete', -> - $(this).find('.btn-save').enableButton() + $(this).find('.btn-save').enable() $(this).find('.loading-gif').hide() $('.update-notifications').on 'ajax:complete', -> - $(this).find('.btn-save').enableButton() - + $(this).find('.btn-save').enable() $('.js-choose-user-avatar-button').bind "click", -> form = $(this).closest("form") diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 5a9cc66c8f0..eb8c1fa1426 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -16,5 +16,11 @@ class @Project $('.hide-no-ssh-message').on 'click', (e) -> path = '/' $.cookie('hide_no_ssh_message', 'false', { path: path }) - $(@).parents('.no-ssh-key-message').hide() + $(@).parents('.no-ssh-key-message').remove() + e.preventDefault() + + $('.hide-no-password-message').on 'click', (e) -> + path = '/' + $.cookie('hide_no_password_message', 'false', { path: path }) + $(@).parents('.no-password-message').remove() e.preventDefault() diff --git a/app/assets/javascripts/project_avatar.js.coffee b/app/assets/javascripts/project_avatar.js.coffee new file mode 100644 index 00000000000..8bec6e2ccca --- /dev/null +++ b/app/assets/javascripts/project_avatar.js.coffee @@ -0,0 +1,9 @@ +class @ProjectAvatar + constructor: -> + $('.js-choose-project-avatar-button').bind 'click', -> + form = $(this).closest('form') + form.find('.js-project-avatar-input').click() + $('.js-project-avatar-input').bind 'change', -> + form = $(this).closest('form') + filename = $(this).val().replace(/^.*[\\\/]/, '') + form.find('.js-avatar-filename').text(filename) diff --git a/app/assets/javascripts/project_members.js.coffee b/app/assets/javascripts/project_members.js.coffee new file mode 100644 index 00000000000..896ba7e53ee --- /dev/null +++ b/app/assets/javascripts/project_members.js.coffee @@ -0,0 +1,4 @@ +class @ProjectMembers + constructor: -> + $('li.project_member').bind 'ajax:success', -> + $(this).fadeOut() diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee index f4a2ca813d2..836269c44f9 100644 --- a/app/assets/javascripts/project_new.js.coffee +++ b/app/assets/javascripts/project_new.js.coffee @@ -9,17 +9,3 @@ class @ProjectNew initEvents: -> disableButtonIfEmptyField '#project_name', '.project-submit' - - $('#project_issues_enabled').change -> - if ($(this).is(':checked') == true) - $('#project_issues_tracker').removeAttr('disabled') - else - $('#project_issues_tracker').attr('disabled', 'disabled') - - $('#project_issues_tracker').change() - - $('#project_issues_tracker').change -> - if ($(this).val() == gon.default_issues_tracker || $(this).is(':disabled')) - $('#project_issues_tracker_id').attr('disabled', 'disabled') - else - $('#project_issues_tracker_id').removeAttr('disabled') diff --git a/app/assets/javascripts/project_show.js.coffee b/app/assets/javascripts/project_show.js.coffee index 02a7d7b731d..6828ae471e5 100644 --- a/app/assets/javascripts/project_show.js.coffee +++ b/app/assets/javascripts/project_show.js.coffee @@ -6,7 +6,7 @@ class @ProjectShow new Flash('Star toggle failed. Try again later.', 'alert') $("a[data-toggle='tab']").on "shown.bs.tab", (e) -> - $.cookie "default_view", $(e.target).attr("href") + $.cookie "default_view", $(e.target).attr("href"), { expires: 30, path: '/' } defaultView = $.cookie("default_view") if defaultView diff --git a/app/assets/javascripts/project_users_select.js.coffee b/app/assets/javascripts/project_users_select.js.coffee deleted file mode 100644 index 7fb33926096..00000000000 --- a/app/assets/javascripts/project_users_select.js.coffee +++ /dev/null @@ -1,58 +0,0 @@ -class @ProjectUsersSelect - constructor: -> - $('.ajax-project-users-select').each (i, select) => - project_id = $(select).data('project-id') || $('body').data('project-id') - - $(select).select2 - placeholder: $(select).data('placeholder') || "Search for a user" - multiple: $(select).hasClass('multiselect') - minimumInputLength: 0 - query: (query) -> - Api.projectUsers project_id, query.term, (users) -> - data = { results: users } - - nullUser = { - name: 'Unassigned', - avatar: null, - username: 'none', - id: '' - } - - data.results.unshift(nullUser) - - query.callback(data) - - initSelection: (element, callback) -> - id = $(element).val() - if id isnt "" - Api.user(id, callback) - - - formatResult: (args...) => - @formatResult(args...) - formatSelection: (args...) => - @formatSelection(args...) - dropdownCssClass: "ajax-project-users-dropdown" - dropdownAutoWidth: true - escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results - m - - formatResult: (user) -> - if user.avatar_url - avatar = user.avatar_url - else - avatar = gon.default_avatar_url - - if user.id == '' - avatarMarkup = '' - else - avatarMarkup = "<div class='user-image'><img class='avatar s24' src='#{avatar}'></div>" - - "<div class='user-result'> - #{avatarMarkup} - <div class='user-name'>#{user.name}</div> - <div class='user-username'>#{user.username}</div> - </div>" - - formatSelection: (user) -> - user.name diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee new file mode 100644 index 00000000000..c0e36d1ccc5 --- /dev/null +++ b/app/assets/javascripts/projects_list.js.coffee @@ -0,0 +1,24 @@ +class @ProjectsList + constructor: -> + $(".projects-list .js-expand").on 'click', (e) -> + e.preventDefault() + list = $(this).closest('.projects-list') + list.find("li").show() + list.find("li.bottom").hide() + + $(".projects-list-filter").keyup -> + terms = $(this).val() + uiBox = $(this).closest('.panel') + if terms == "" || terms == undefined + uiBox.find(".projects-list li").show() + else + uiBox.find(".projects-list li").each (index) -> + name = $(this).find(".filter-title").text() + + if name.toLowerCase().search(terms.toLowerCase()) == -1 + $(this).hide() + else + $(this).show() + uiBox.find(".projects-list li.bottom").hide() + + diff --git a/app/assets/javascripts/protected_branches.js.coffee b/app/assets/javascripts/protected_branches.js.coffee new file mode 100644 index 00000000000..5753c9d4e72 --- /dev/null +++ b/app/assets/javascripts/protected_branches.js.coffee @@ -0,0 +1,21 @@ +$ -> + $(".protected-branches-list :checkbox").change (e) -> + name = $(this).attr("name") + if name == "developers_can_push" + id = $(this).val() + checked = $(this).is(":checked") + url = $(this).data("url") + $.ajax + type: "PUT" + url: url + dataType: "json" + data: + id: id + developers_can_push: checked + + success: -> + row = $(e.target) + row.closest('tr').effect('highlight') + + error: -> + new Flash("Failed to update branch!", "alert") diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee index d522d9f3b90..4a05bdccdb3 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee @@ -3,12 +3,12 @@ class @ShortcutsDashboardNavigation extends Shortcuts constructor: -> super() - Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-activity')) - Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-projects')) - Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-issues')) - Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-merge_requests')) + 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')) - @findAndollowLink: (selector) -> + @findAndFollowLink: (selector) -> link = $(selector).attr('href') if link window.location = link diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee new file mode 100644 index 00000000000..bb532194682 --- /dev/null +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -0,0 +1,46 @@ +#= require mousetrap +#= require shortcuts_navigation + +class @ShortcutsIssuable extends ShortcutsNavigation + constructor: (isMergeRequest) -> + super() + Mousetrap.bind('a', -> + $('.js-assignee').select2('open') + return false + ) + Mousetrap.bind('m', -> + $('.js-milestone').select2('open') + return false + ) + Mousetrap.bind('r', => + @replyWithSelectedText() + return false + ) + + if isMergeRequest + @enabledHelp.push('.hidden-shortcut.merge_requests') + else + @enabledHelp.push('.hidden-shortcut.issues') + + replyWithSelectedText: -> + if window.getSelection + selected = window.getSelection().toString() + replyField = $('.js-main-target-form #note_note') + + return if selected.trim() == "" + + # Put a '>' character before each non-empty line in the selection + quote = _.map selected.split("\n"), (val) -> + "> #{val}\n" if val.trim() != '' + + # If replyField already has some content, add a newline before our quote + separator = replyField.val().trim() != "" and "\n" or '' + + replyField.val (_, current) -> + current + separator + quote.join('') + "\n" + + # Trigger autosave for the added text + replyField.trigger('input') + + # Focus the input field + replyField.focus() diff --git a/app/assets/javascripts/shortcuts_issueable.coffee b/app/assets/javascripts/shortcuts_issueable.coffee deleted file mode 100644 index b8dae71e037..00000000000 --- a/app/assets/javascripts/shortcuts_issueable.coffee +++ /dev/null @@ -1,19 +0,0 @@ -#= require shortcuts_navigation - -class @ShortcutsIssueable extends ShortcutsNavigation - constructor: (isMergeRequest) -> - super() - Mousetrap.bind('a', -> - $('.js-assignee').select2('open') - return false - ) - Mousetrap.bind('m', -> - $('.js-milestone').select2('open') - return false - ) - - if isMergeRequest - @enabledHelp.push('.hidden-shortcut.merge_reuests') - else - @enabledHelp.push('.hidden-shortcut.issues') - diff --git a/app/assets/javascripts/shortcuts_navigation.coffee b/app/assets/javascripts/shortcuts_navigation.coffee index e592b700e7c..31895fbf2bc 100644 --- a/app/assets/javascripts/shortcuts_navigation.coffee +++ b/app/assets/javascripts/shortcuts_navigation.coffee @@ -3,18 +3,18 @@ class @ShortcutsNavigation extends Shortcuts constructor: -> super() - Mousetrap.bind('g p', -> ShortcutsNavigation.findAndollowLink('.shortcuts-project')) - Mousetrap.bind('g f', -> ShortcutsNavigation.findAndollowLink('.shortcuts-tree')) - Mousetrap.bind('g c', -> ShortcutsNavigation.findAndollowLink('.shortcuts-commits')) - Mousetrap.bind('g n', -> ShortcutsNavigation.findAndollowLink('.shortcuts-network')) - Mousetrap.bind('g g', -> ShortcutsNavigation.findAndollowLink('.shortcuts-graphs')) - Mousetrap.bind('g i', -> ShortcutsNavigation.findAndollowLink('.shortcuts-issues')) - Mousetrap.bind('g m', -> ShortcutsNavigation.findAndollowLink('.shortcuts-merge_requests')) - Mousetrap.bind('g w', -> ShortcutsNavigation.findAndollowLink('.shortcuts-wiki')) - Mousetrap.bind('g s', -> ShortcutsNavigation.findAndollowLink('.shortcuts-snippets')) + Mousetrap.bind('g p', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project')) + Mousetrap.bind('g f', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-tree')) + Mousetrap.bind('g c', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-commits')) + Mousetrap.bind('g n', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-network')) + Mousetrap.bind('g g', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs')) + Mousetrap.bind('g i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-issues')) + Mousetrap.bind('g m', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests')) + Mousetrap.bind('g w', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki')) + Mousetrap.bind('g s', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets')) @enabledHelp.push('.hidden-shortcut.project') - - @findAndollowLink: (selector) -> + + @findAndFollowLink: (selector) -> link = $(selector).attr('href') if link window.location = link diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index c084d730d62..fb08016fbae 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -1,26 +1,10 @@ -responsive_resize = -> - current_width = $(window).width() - if current_width < 985 - $('.responsive-side').addClass("ui right wide sidebar") - else - $('.responsive-side').removeClass("ui right wide sidebar") - -$ -> - # Depending on window size, set the sidebar offscreen. - responsive_resize() - - $('.sidebar-expand-button').click -> - $('.ui.sidebar') - .sidebar({overlay: true}) - .sidebar('toggle') - - # Hide sidebar on click outside of sidebar - $(document).mouseup (e) -> - container = $(".ui.sidebar") - container.sidebar "hide" if not container.is(e.target) and container.has(e.target).length is 0 - return - -# On resize, check if sidebar should be offscreen. -$(window).resize -> - responsive_resize() - return +$(document).on("click", '.toggle-nav-collapse', (e) -> + e.preventDefault() + collapsed = 'page-sidebar-collapsed' + expanded = 'page-sidebar-expanded' + + $('.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: '/' }) +) diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/stat_graph_contributors.js.coffee index 27f0fd31d50..3be14cb43dd 100644 --- a/app/assets/javascripts/stat_graph_contributors.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors.js.coffee @@ -1,3 +1,6 @@ +#= require d3 +#= require stat_graph_contributors_util + class @ContributorsStatGraph init: (log) -> @parsed_log = ContributorsStatGraphUtil.parse_log(log) diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee index 9952fa0b00a..b7a0e073766 100644 --- a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee @@ -1,3 +1,7 @@ +#= require d3 +#= require jquery +#= require underscore + class @ContributorsGraph MARGIN: top: 20 @@ -46,7 +50,7 @@ class @ContributorsGraph class @ContributorsMasterGraph extends ContributorsGraph constructor: (@data) -> - @width = $('.container').width() - 70 + @width = $('.content').width() - 70 @height = 200 @x = null @y = null @@ -119,7 +123,7 @@ class @ContributorsMasterGraph extends ContributorsGraph class @ContributorsAuthorGraph extends ContributorsGraph constructor: (@data) -> - @width = $('.container').width()/2 - 100 + @width = $('.content').width()/2 - 100 @height = 200 @x = null @y = null diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee index 1670f5c7bc1..cfe5508290f 100644 --- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee @@ -2,11 +2,15 @@ window.ContributorsStatGraphUtil = parse_log: (log) -> total = {} by_author = {} + by_email = {} for entry in log @add_date(entry.date, total) unless total[entry.date]? - @add_author(entry, by_author) unless by_author[entry.author_name]? - @add_date(entry.date, by_author[entry.author_name]) unless by_author[entry.author_name][entry.date] - @store_data(entry, total[entry.date], by_author[entry.author_name][entry.date]) + + data = by_author[entry.author_name] #|| by_email[entry.author_email] + data ?= @add_author(entry, by_author, by_email) + + @add_date(entry.date, data) unless data[entry.date] + @store_data(entry, total[entry.date], data[entry.date]) total = _.toArray(total) by_author = _.toArray(by_author) total: total, by_author: by_author @@ -15,10 +19,12 @@ window.ContributorsStatGraphUtil = collection[date] = {} collection[date].date = date - add_author: (author, by_author) -> - by_author[author.author_name] = {} - by_author[author.author_name].author_name = author.author_name - by_author[author.author_name].author_email = author.author_email + add_author: (author, by_author, by_email) -> + data = {} + data.author_name = author.author_name + data.author_email = author.author_email + by_author[author.author_name] = data + by_email[author.author_email] = data store_data: (entry, total, by_author) -> @store_commits(total, by_author) diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee new file mode 100644 index 00000000000..7f41616d4e7 --- /dev/null +++ b/app/assets/javascripts/subscription.js.coffee @@ -0,0 +1,17 @@ +class @Subscription + constructor: (url) -> + $(".subscribe-button").unbind("click").click (event)=> + btn = $(event.currentTarget) + action = btn.find("span").text() + current_status = $(".subscription-status").attr("data-status") + btn.prop("disabled", true) + + $.post url, => + btn.prop("disabled", false) + status = if current_status == "subscribed" then "unsubscribed" else "subscribed" + $(".subscription-status").attr("data-status", status) + action = if status == "subscribed" then "Unsubscribe" else "Subscribe" + btn.find("span").text(action) + $(".subscription-status>div").toggleClass("hidden") + + diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee index 8a2e2421c2e..d0d81f96921 100644 --- a/app/assets/javascripts/user.js.coffee +++ b/app/assets/javascripts/user.js.coffee @@ -1,3 +1,4 @@ class @User constructor: -> $('.profile-groups-avatars').tooltip("placement": "top") + new ProjectsList() diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 9eee7406511..aeeed9ca3cc 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -1,20 +1,66 @@ class @UsersSelect constructor: -> + @usersPath = "/autocomplete/users.json" + @userPath = "/autocomplete/users/:id.json" + $('.ajax-users-select').each (i, select) => + @projectId = $(select).data('project-id') + @groupId = $(select).data('group-id') + showNullUser = $(select).data('null-user') + showAnyUser = $(select).data('any-user') + showEmailUser = $(select).data('email-user') + firstUser = $(select).data('first-user') + $(select).select2 placeholder: "Search for a user" multiple: $(select).hasClass('multiselect') minimumInputLength: 0 - query: (query) -> - Api.users query.term, (users) -> + query: (query) => + @users query.term, (users) => data = { results: users } + + if query.term.length == 0 + if firstUser + # Move current user to the front of the list + for obj, index in data.results + if obj.username == firstUser + data.results.splice(index, 1) + data.results.unshift(obj) + break + + if showNullUser + nullUser = { + name: 'Unassigned', + avatar: null, + username: 'none', + id: 0 + } + data.results.unshift(nullUser) + + if showAnyUser + anyUser = { + name: 'Any', + avatar: null, + username: 'none', + id: null + } + data.results.unshift(anyUser) + + if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/) + emailUser = { + name: "Invite \"#{query.term}\"", + avatar: null, + username: query.term, + id: query.term + } + data.results.unshift(emailUser) + query.callback(data) - initSelection: (element, callback) -> + initSelection: (element, callback) => id = $(element).val() - if id isnt "" - Api.user(id, callback) - + if id != "" && id != "0" + @user(id, callback) formatResult: (args...) => @formatResult(args...) @@ -38,3 +84,34 @@ class @UsersSelect formatSelection: (user) -> user.name + + user: (user_id, callback) => + url = @buildUrl(@userPath) + url = url.replace(':id', user_id) + + $.ajax( + url: url + dataType: "json" + ).done (user) -> + callback(user) + + # Return users list. Filtered by query + # Only active users retrieved + users: (query, callback) => + url = @buildUrl(@usersPath) + + $.ajax( + url: url + data: + search: query + per_page: 20 + active: true + project_id: @projectId + group_id: @groupId + dataType: "json" + ).done (users) -> + callback(users) + + buildUrl: (url) -> + url = gon.relative_url_root + url if gon.relative_url_root? + return url diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee index 66757565d3a..81cfc37b956 100644 --- a/app/assets/javascripts/wikis.js.coffee +++ b/app/assets/javascripts/wikis.js.coffee @@ -1,9 +1,17 @@ class @Wikis constructor: -> - $('.build-new-wiki').bind "click", -> + $('.build-new-wiki').bind "click", (e) -> + $('[data-error~=slug]').addClass("hidden") + $('p.hint').show() field = $('#new_wiki_path') - slug = field.val() - path = field.attr('data-wikis-path') + valid_slug_pattern = /^[\w\/-]+$/ - if(slug.length > 0) - location.href = path + "/" + slug + slug = field.val() + if slug.match valid_slug_pattern + path = field.attr('data-wikis-path') + if(slug.length > 0) + location.href = path + "/" + slug + else + e.preventDefault() + $('p.hint').hide() + $('[data-error~=slug]').removeClass("hidden") diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee index 0c9942a4014..8a0564a9098 100644 --- a/app/assets/javascripts/zen_mode.js.coffee +++ b/app/assets/javascripts/zen_mode.js.coffee @@ -1,6 +1,8 @@ -class @ZenMode - @fullscreen_prefix = 'fullscreen_' +#= require dropzone +#= require mousetrap +#= require mousetrap/pause +class @ZenMode constructor: -> @active_zen_area = null @active_checkbox = null @@ -10,30 +12,33 @@ class @ZenMode if not @active_checkbox @scroll_position = window.pageYOffset - $('body').on 'change', '.zennable input[type=checkbox]', (e) => + $('body').on 'click', '.zen-enter-link', (e) => + e.preventDefault() + $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', true).change() + + $('body').on 'click', '.zen-leave-link', (e) => + e.preventDefault() + $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', false).change() + + $('body').on 'change', '.zen-toggle-comment', (e) => checkbox = e.currentTarget if checkbox.checked # Disable other keyboard shortcuts in ZEN mode Mousetrap.pause() - @udpateActiveZenArea(checkbox) + @updateActiveZenArea(checkbox) else @exitZenMode() $(document).on 'keydown', (e) => - if e.keyCode is $.ui.keyCode.ESCAPE + if e.keyCode is 27 # Esc @exitZenMode() e.preventDefault() - $(window).on 'hashchange', @updateZenModeFromLocationHash - - udpateActiveZenArea: (checkbox) => + updateActiveZenArea: (checkbox) => @active_checkbox = $(checkbox) @active_checkbox.prop('checked', true) @active_zen_area = @active_checkbox.parent().find('textarea') @active_zen_area.focus() - window.location.hash = ZenMode.fullscreen_prefix + @active_checkbox.prop('id') - # Disable dropzone in ZEN mode - Dropzone.forElement('.div-dropzone').disable() exitZenMode: => if @active_zen_area isnt null @@ -41,21 +46,9 @@ class @ZenMode @active_checkbox.prop('checked', false) @active_zen_area = null @active_checkbox = null - window.location.hash = '' - window.scrollTo(window.pageXOffset, @scroll_position) + @restoreScroll(@scroll_position) # Enable dropzone when leaving ZEN mode Dropzone.forElement('.div-dropzone').enable() - checkboxFromLocationHash: (e) -> - id = $.trim(window.location.hash.replace('#' + ZenMode.fullscreen_prefix, '')) - if id - return $('.zennable input[type=checkbox]#' + id)[0] - else - return null - - updateZenModeFromLocationHash: (e) => - checkbox = @checkboxFromLocationHash() - if checkbox - @udpateActiveZenArea(checkbox) - else - @exitZenMode() + restoreScroll: (y) -> + window.scrollTo(window.pageXOffset, y) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 0d404f15055..1a5f11df7d1 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -6,17 +6,22 @@ *= require jquery.ui.autocomplete *= require jquery.atwho *= require select2 - *= require highlightjs.min *= require_self *= require dropzone/basic + *= require cal-heatmap */ -@import "main/*"; + +@import "base/variables"; +@import "base/mixins"; +@import "base/layout"; + /** * Customized Twitter bootstrap */ -@import 'gl_bootstrap'; +@import 'base/gl_variables'; +@import 'base/gl_bootstrap'; /** * NProgress load bar css @@ -31,32 +36,27 @@ @import "font-awesome"; /** + * UI themes: + */ +@import "themes/**/*"; + +/** * Generic css (forms, nav etc): */ -@import "generic/*"; +@import "generic/**/*"; /** * Page specific styles (issues, projects etc): */ -@import "sections/*"; +@import "pages/**/*"; /** * Code highlight */ -@import "highlight/*"; - -/** - * UI themes: - */ -@import "themes/*"; +@import "highlight/**/*"; /** * Styles for JS behaviors. */ @import "behaviors.scss"; - -/** -* Styles for responsive sidebar -*/ -@import "semantic-ui/modules/sidebar"; diff --git a/app/assets/stylesheets/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss index 9c5e76ab8e2..21acbfa5e5a 100644 --- a/app/assets/stylesheets/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -1,16 +1,8 @@ /* * Twitter bootstrap with GitLab customizations/additions * - * Some unused bootstrap compontents like panels are not included. - * Other components like tabs are modified to GitLab style. - * */ -$font-size-base: 13px !default; -$nav-pills-active-link-hover-bg: $bg_primary; -$pagination-active-bg: $bg_primary; -$list-group-active-bg: $bg_primary; - // Core variables and mixins @import "bootstrap/variables"; @import "bootstrap/mixins"; @@ -26,6 +18,7 @@ $list-group-active-bg: $bg_primary; @import "bootstrap/grid"; @import "bootstrap/tables"; @import "bootstrap/forms"; +@import "bootstrap/buttons"; // Components @import "bootstrap/component-animations"; @@ -124,7 +117,7 @@ $list-group-active-bg: $bg_primary; color: #888; text-shadow: 0 1px 1px #fff; } - i[class~="fa"] { + i.fa { line-height: 14px; } } @@ -137,10 +130,6 @@ $list-group-active-bg: $bg_primary; } } } - - &.nav-small-tabs > li > a { - padding: 6px 9px; - } } .nav-tabs > li > a, @@ -148,57 +137,12 @@ $list-group-active-bg: $bg_primary; color: #666; } -.nav-small > li > a { - padding: 3px 5px; - font-size: 12px; +.nav-pills > .active > a > span > .badge { + background-color: #fff; + color: $gl-primary; } -/* - * Callouts from Bootstrap3 docs - * - * Not quite alerts, but custom and helpful notes for folks reading the docs. - * Requires a base and modifier class. - */ - -/* Common styles for all types */ -.bs-callout { - margin: 20px 0; - padding: 20px; - border-left: 3px solid #eee; - color: #666; - background: #f9f9f9; -} -.bs-callout h4 { - margin-top: 0; - margin-bottom: 5px; -} -.bs-callout p:last-child { - margin-bottom: 0; -} - -/* Variations */ -.bs-callout-danger { - background-color: #fdf7f7; - border-color: #eed3d7; - color: #b94a48; -} -.bs-callout-warning { - background-color: #faf8f0; - border-color: #faebcc; - color: #8a6d3b; -} -.bs-callout-info { - background-color: #f4f8fa; - border-color: #bce8f1; - color: #34789a; -} -.bs-callout-success { - background-color: #dff0d8; - border-color: #5cA64d; - color: #3c763d; -} - /** * fix to keep tooltips position in top navigation bar * @@ -213,16 +157,12 @@ $list-group-active-bg: $bg_primary; * */ .panel { - @include border-radius(0px); - .panel-heading { - @include border-radius(0px); - font-size: 14px; - line-height: 18px; + font-weight: bold; .panel-head-actions { position: relative; - top: -7px; + top: -5px; float: right; } } @@ -248,6 +188,7 @@ $list-group-active-bg: $bg_primary; .panel-heading { padding: 6px 15px; font-size: 13px; + font-weight: normal; a { color: #777; } @@ -255,40 +196,62 @@ $list-group-active-bg: $bg_primary; } } -.panel-default { - .panel-heading { - background-color: #EEE; +.panel-succes .panel-heading, +.panel-info .panel-heading, +.panel-danger .panel-heading, +.panel-warning .panel-heading, +.panel-primary .panel-heading, +.alert { + a:not(.btn) { + @extend .alert-link; + color: #fff; + text-decoration: underline; } } -.panel-danger { - @include panel-colored; - .panel-heading { - color: $border_danger; - border-color: $border_danger; - } +// Typography ================================================================= + +.text-primary, +.text-primary:hover { + color: $brand-primary; } -.panel-success { - @include panel-colored; - .panel-heading { - color: $border_success; - border-color: $border_success; - } +.text-success, +.text-success:hover { + color: $brand-success; } -.panel-primary { - @include panel-colored; - .panel-heading { - color: $border_primary; - border-color: $border_primary; - } +.text-danger, +.text-danger:hover { + color: $brand-danger; } -.panel-warning { - @include panel-colored; - .panel-heading { - color: $border_warning; - border-color: $border_warning; +.text-warning, +.text-warning:hover { + color: $brand-warning; +} + +.text-info, +.text-info:hover { + color: $brand-info; +} + +// Tables ===================================================================== + +table.table { + .dropdown-menu a { + text-decoration: none; + } + + .success, + .warning, + .danger, + .info { + color: #fff; + + a:not(.btn) { + text-decoration: underline; + color: #fff; + } } } diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss new file mode 100644 index 00000000000..56f4c794e1b --- /dev/null +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -0,0 +1,133 @@ +// Override Bootstrap variables here (defaults from bootstrap-sass v3.3.3): +// For all variables see https://github.com/twbs/bootstrap-sass/blob/master/templates/project/_bootstrap-variables.sass +// +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +// $gray-base: #000 +// $gray-darker: lighten($gray-base, 13.5%) // #222 +// $gray-dark: lighten($gray-base, 20%) // #333 +// $gray: lighten($gray-base, 33.5%) // #555 +// $gray-light: lighten($gray-base, 46.7%) // #777 +// $gray-lighter: lighten($gray-base, 93.5%) // #eee + +$brand-primary: $gl-primary; +$brand-success: $gl-success; +$brand-info: $gl-info; +$brand-warning: $gl-warning; +$brand-danger: $gl-danger; + + +//== Scaffolding +// +$text-color: $gl-text-color; +$link-color: $gl-link-color; + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +$font-family-sans-serif: $regular_font; +$font-family-monospace: $monospace_font; +$font-size-base: $gl-font-size; + + +//== Components +// +//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). + +$padding-base-vertical: 6px; +$padding-base-horizontal: 14px; + + +//== Forms +// +//## + +$input-color: $text-color; +$input-border: #DDD; +$input-border-focus: $brand-info; +$legend-color: $text-color; + + +//== Pagination +// +//## + +$pagination-color: #fff; +$pagination-bg: $brand-success; +$pagination-border: transparent; + +$pagination-hover-color: #fff; +$pagination-hover-bg: darken($brand-success, 15%); +$pagination-hover-border: transparent; + +$pagination-active-color: #fff; +$pagination-active-bg: darken($brand-success, 15%); +$pagination-active-border: transparent; + +$pagination-disabled-color: #b4bcc2; +$pagination-disabled-bg: lighten($brand-success, 15%); +$pagination-disabled-border: transparent; + + +//== Form states and alerts +// +//## Define colors for form feedback states and, by default, alerts. + +$state-success-text: #fff; +$state-success-bg: $brand-success; +$state-success-border: $brand-success; + +$state-info-text: #fff; +$state-info-bg: $brand-info; +$state-info-border: $brand-info; + +$state-warning-text: #fff; +$state-warning-bg: $brand-warning; +$state-warning-border: $brand-warning; + +$state-danger-text: #fff; +$state-danger-bg: $brand-danger; +$state-danger-border: $brand-danger; + + +//== Alerts +// +//## Define alert colors, border radius, and padding. + +$alert-border-radius: 0; + + +//== Panels +// +//## + +$panel-border-radius: 0; +$panel-default-text: $text-color; +$panel-default-border: $border-color; +$panel-default-heading-bg: $background-color; + + +//== Wells +// +//## + +$well-bg: #F9F9F9; +$well-border: #EEE; + +//== Code +// +//## + +$code-color: #c7254e; +$code-bg: #f9f2f4; + +$kbd-color: #fff; +$kbd-bg: #333; diff --git a/app/assets/stylesheets/main/layout.scss b/app/assets/stylesheets/base/layout.scss index 2800feb81f2..690d89a5c16 100644 --- a/app/assets/stylesheets/main/layout.scss +++ b/app/assets/stylesheets/base/layout.scss @@ -2,10 +2,10 @@ html { overflow-y: scroll; &.touch .tooltip { display: none !important; } -} -body { - padding-bottom: 20px; + body { + padding-top: $header-height; + } } .container { @@ -17,3 +17,6 @@ body { margin: 0 0; } +.navless-container { + margin-top: 30px; +} diff --git a/app/assets/stylesheets/main/mixins.scss b/app/assets/stylesheets/base/mixins.scss index 5f83913b73b..08cbe911672 100644 --- a/app/assets/stylesheets/main/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -21,6 +21,10 @@ @include border-radius($radius 0 0 $radius) } +@mixin border-radius-right($radius) { + @include border-radius(0 0 $radius $radius) +} + @mixin linear-gradient($from, $to) { background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to)); background-image: -webkit-linear-gradient($from, $to); @@ -50,13 +54,6 @@ @include box-shadow(0 0 0 3px #f1f1f1); } -@mixin header-font { - color: $style_color; - font-size: 16px; - line-height: 44px; - font-weight: normal; -} - @mixin md-typography { font-size: 15px; line-height: 1.5; @@ -69,7 +66,28 @@ margin-top: 0; } - code { padding: 0 4px; } + code { + font-family: $monospace_font; + white-space: pre; + word-wrap: normal; + padding: 0; + } + + kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #555; + vertical-align: middle; + background-color: #FCFCFC; + border-width: 1px; + border-style: solid; + border-color: #CCC #CCC #BBB; + border-image: none; + border-radius: 3px; + box-shadow: 0px -1px 0px #BBB inset; + } h1 { margin-top: 45px; @@ -108,20 +126,27 @@ p > code { font-size: inherit; font-weight: inherit; - color: #555; } li { line-height: 1.5; } -} -@mixin page-title { - color: #333; - line-height: 1.5; - font-weight: normal; - margin-top: 0px; - margin-bottom: 10px; + a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] { + &:before { + margin-right: 4px; + + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + content: "\f0c6"; + } + + &:hover:before { + text-decoration: none; + } + } } @mixin str-truncated($max_width: 82%) { @@ -132,14 +157,3 @@ white-space: nowrap; max-width: $max_width; } - -@mixin panel-colored { - border: none; - background: $box_bg; - @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); - - .panel-heading { - font-weight: bold; - background-color: $box_bg; - } -} diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss new file mode 100644 index 00000000000..08f153dfbc9 --- /dev/null +++ b/app/assets/stylesheets/base/variables.scss @@ -0,0 +1,37 @@ +$style_color: #474D57; +$hover: #FFFAF1; +$gl-text-color: #222222; +$gl-link-color: #446e9b; +$nprogress-color: #c0392b; +$gl-font-size: 14px; +$list-font-size: 15px; +$sidebar_collapsed_width: 52px; +$sidebar_width: 230px; +$avatar_radius: 50%; +$code_font_size: 13px; +$code_line_height: 1.5; +$border-color: #E5E5E5; +$background-color: #f5f5f5; +$header-height: 50px; + + +/* + * State colors: + */ +$gl-primary: #446e9b; +$gl-success: #019875; +$gl-info: #029ACF; +$gl-warning: #EB9532; +$gl-danger: #d9534f; + +/* + * Commit Diff Colors + */ +$added: #63c363; +$deleted: #f77; + +/* + * Fonts + */ +$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; +$regular_font: "Helvetica Neue", Helvetica, Arial, sans-serif; diff --git a/app/assets/stylesheets/generic/avatar.scss b/app/assets/stylesheets/generic/avatar.scss index 4f038b977e2..8595887c3b9 100644 --- a/app/assets/stylesheets/generic/avatar.scss +++ b/app/assets/stylesheets/generic/avatar.scss @@ -2,15 +2,21 @@ float: left; margin-right: 12px; width: 40px; - padding: 1px; - @include border-radius(4px); + height: 40px; + padding: 0; + @include border-radius($avatar_radius); &.avatar-inline { float: none; - margin-left: 3px; + margin-left: 4px; + margin-bottom: 2px; - &.s16 { margin-right: 2px; } - &.s24 { margin-right: 2px; } + &.s16 { margin-right: 4px; } + &.s24 { margin-right: 4px; } + } + + &.group-avatar, &.project-avatar, &.avatar-tile { + @include border-radius(0px); } &.s16 { width: 16px; height: 16px; margin-right: 6px; } @@ -21,3 +27,16 @@ &.s90 { width: 90px; height: 90px; margin-right: 15px; } &.s160 { width: 160px; height: 160px; margin-right: 20px; } } + +.identicon { + text-align: center; + vertical-align: top; + + &.s16 { font-size: 12px; line-height: 1.33; } + &.s24 { font-size: 14px; line-height: 1.8; } + &.s26 { font-size: 20px; line-height: 1.33; } + &.s32 { font-size: 22px; line-height: 32px; } + &.s60 { font-size: 32px; line-height: 60px; } + &.s90 { font-size: 36px; line-height: 90px; } + &.s160 { font-size: 96px; line-height: 1.33; } +} diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index d098f1ecaa2..cd6bf64c0ae 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -1,127 +1,15 @@ .btn { - display: inline-block; - margin-bottom: 0; - font-weight: normal; - text-align: center; - vertical-align: middle; - cursor: pointer; - background-image: none; - border: $btn-border; - white-space: nowrap; - padding: 6px 12px; - font-size: 13px; - line-height: 18px; - border-radius: 4px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; - color: #444444; - background-color: #fff; - text-shadow: none; - - &.hover, - &:hover { - color: #444444; - text-decoration: none; - background-color: #ebebeb; - border-color: #adadad; - } - - &.focus, - &:focus { - color: #444444; - text-decoration: none; - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; - } - - &.active, - &:active { - outline: 0; - background-image: none; - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - } - - &.disabled, - &[disabled] { - cursor: not-allowed; - pointer-events: none; - opacity: 0.65; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - box-shadow: none; - } - - &.btn-primary { - color: #ffffff; - background-color: $bg_primary; - border-color: $border_primary; - - &.hover, - &:hover, - &.disabled, - &[disabled] { - color: #ffffff; - } - } - - &.btn-success { - color: #ffffff; - background-color: $bg_success; - border-color: $border_success; - - - &.hover, - &:hover, - &.disabled, - &[disabled] { - color: #ffffff; - } - } - - &.btn-danger { - color: #ffffff; - background-color: $bg_danger; - border-color: $border_danger; - - - &.hover, - &:hover, - &.disabled, - &[disabled] { - color: #ffffff; - } - } - - &.btn-warning { - color: #ffffff; - background-color: $bg_warning; - border-color: $border_warning; - - - &.hover, - &:hover, - &.disabled, - &[disabled] { - color: #ffffff; - } - } + @extend .btn-default; &.btn-new { @extend .btn-success; } &.btn-create { - @extend .wide; @extend .btn-success; } &.btn-save { - @extend .wide; @extend .btn-primary; } @@ -133,34 +21,17 @@ float: right; } - &.wide { - padding-left: 20px; - padding-right: 20px; - } - - &.btn-small { - padding: 2px 10px; - font-size: 12px; - } - - &.btn-tiny { - font-size: 11px; - padding: 2px 6px; - line-height: 16px; - margin: 2px; - } - &.btn-close { - color: $bg_danger; - border-color: $border_danger; + color: $gl-danger; + border-color: $gl-danger; &:hover { color: #B94A48; } } &.btn-reopen { - color: $bg_success; - border-color: $border_success; + color: $gl-success; + border-color: $gl-success; &:hover { color: #468847; } @@ -173,6 +44,14 @@ margin-right: 0px; } } + + &.btn-save { + @extend .btn-primary; + } + + &.btn-new, &.btn-create { + @extend .btn-success; + } } .btn-block { @@ -193,6 +72,3 @@ } } } - -.btn-group-small > .btn { @extend .btn.btn-small; } -.btn-group-tiny > .btn { @extend .btn.btn-tiny; } diff --git a/app/assets/stylesheets/generic/calendar.scss b/app/assets/stylesheets/generic/calendar.scss new file mode 100644 index 00000000000..a36fefe22c5 --- /dev/null +++ b/app/assets/stylesheets/generic/calendar.scss @@ -0,0 +1,90 @@ +.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; + } + } +} +/** +* This overwrites the default values of the cal-heatmap gem +*/ +.calendar { + .qi { + background-color: #999; + fill: #fff; + } + + .q1 { + background-color: #dae289; + fill: #ededed; + } + + .q2 { + background-color: #cedb9c; + fill: #ACD5F2; + } + + .q3 { + background-color: #b5cf6b; + fill: #7FA8D1; + } + + .q4 { + background-color: #637939; + fill: #49729B; + } + + .q5 { + background-color: #3b6427; + fill: #254E77; + } + + .domain-background { + fill: none; + shape-rendering: crispedges; + } + + .ch-tooltip { + position: absolute; + display: none; + margin-top: 22px; + margin-left: 1px; + font-size: 13px; + padding: 3px; + font-weight: 550; + background-color: #222; + span { + position: absolute; + width: 200px; + text-align: center; + visibility: hidden; + border-radius: 10px; + &:after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + margin-left: -8px; + width: 0; + height: 0; + border-top: 8px solid #000000; + border-right: 8px solid transparent; + border-left: 8px solid transparent; + } + } + } +} diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 2fc738c18d8..1419a9cded9 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -24,7 +24,7 @@ .slead { color: #666; - font-size: 14px; + font-size: 15px; margin-bottom: 12px; font-weight: normal; line-height: 24px; @@ -54,9 +54,14 @@ pre { text-shadow: none; } +.dropdown-menu-align-right { + left: auto; + right: 0px; +} + .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { - background: $bg_primary; + background: $gl-primary; color: #FFF } @@ -66,7 +71,7 @@ pre { /** FLASH message **/ .author_link { - color: $link_color; + color: $gl-link-color; } .help li { color:$style_color; } @@ -162,7 +167,7 @@ li.note { background-color: inherit; } -.team_member_show { +.project_member_show { td:first-child { color: #aaa; } @@ -207,24 +212,16 @@ li.note { } } -.no-ssh-key-message { - padding: 10px 0; - background: #C67; - margin: 0; - color: #FFF; - margin-top: -1px; +.browser-alert { + padding: 10px; text-align: center; - + background: #C67; + color: #fff; + font-weight: bold; a { color: #fff; text-decoration: underline; } - - .links-xs { - text-align: center; - font-size: 16px; - padding: 5px; - } } .warning_message { @@ -249,7 +246,7 @@ li.note { .milestone { &.milestone-closed { - background: #eee; + background: #f9f9f9; } .progress { margin-bottom: 0; @@ -281,10 +278,6 @@ img.emoji { height: 220px; } -.navless-container { - margin-top: 20px; -} - .description-block { @extend .light-well; @extend .light; @@ -300,23 +293,17 @@ table { .dashboard-intro-icon { float: left; + text-align: center; font-size: 32px; color: #AAA; - padding: 5px 0; - width: 50px; - min-height: 100px; -} - -.broadcast-message { - padding: 10px; - text-align: center; - background: #555; - color: #BBB; + width: 60px; } -.broadcast-message-preview { - @extend .broadcast-message; - margin-bottom: 20px; +.dashboard-intro-text { + display: inline-block; + margin-left: -60px; + padding-left: 60px; + width: 100%; } .btn-sign-in { @@ -334,24 +321,46 @@ table { margin-bottom: 9px; } -.footer-links a { - margin-right: 15px; +.wiki .code { + overflow-x: auto; } -.search_box { - position: relative; - padding: 30px; - text-align: center; - background-color: #F9F9F9; - border: 1px solid #DDDDDD; - border-radius: 0px; +.footer-links { + margin-bottom: 20px; + a { + margin-right: 15px; + } } -.search_glyph { - color: #555; - font-size: 42px; +.search_box { + @extend .well; + text-align: center; } .task-status { margin-left: 10px; } + +#nprogress .spinner { + top: 15px !important; + right: 10px !important; +} + +.header-with-avatar { + h3 { + margin: 0; + font-weight: bold; + } + + .username { + font-size: 18px; + color: #666; + margin-top: 8px; + } + + .description { + font-size: 16px; + color: #666; + margin-top: 8px; + } +} diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/generic/files.scss index 1ed41272ac5..8014dcb165b 100644 --- a/app/assets/stylesheets/generic/files.scss +++ b/app/assets/stylesheets/generic/files.scss @@ -3,7 +3,7 @@ * */ .file-holder { - border: 1px solid #CCC; + border: 1px solid $border-color; margin-bottom: 1em; table { @@ -11,34 +11,30 @@ } .file-title { - background: #EEE; - border-bottom: 1px solid #CCC; + position: relative; + background: $background-color; + border-bottom: 1px solid $border-color; text-shadow: 0 1px 1px #fff; margin: 0; text-align: left; padding: 10px 15px; - .options { + .file-actions { float: right; - margin-top: -3px; + position: absolute; + top: 5px; + right: 15px; + + .btn { + padding: 0px 10px; + font-size: 13px; + line-height: 28px; + } } .left-options { margin-top: -3px; } - - .file_name { - font-weight: bold; - padding-left: 3px; - font-size: 14px; - - small { - color: #888; - font-size: 13px; - font-weight: normal; - padding-left: 10px; - } - } } .file-content { background: #fff; @@ -98,7 +94,7 @@ } .author, .blame_commit { - background: #f5f5f5; + background: $background-color; vertical-align: top; } .lines { @@ -119,7 +115,7 @@ ol { margin-left: 40px; padding: 10px 0; - border-left: 1px solid #CCC; + border-left: 1px solid $border-color; margin-bottom: 0; background: white; li { diff --git a/app/assets/stylesheets/generic/filters.scss b/app/assets/stylesheets/generic/filters.scss new file mode 100644 index 00000000000..bd93a79722d --- /dev/null +++ b/app/assets/stylesheets/generic/filters.scss @@ -0,0 +1,55 @@ +.filter-item { + margin-right: 15px; +} + +.issues-state-filters { + li.active a { + border-color: #DDD !important; + + &, &:hover, &:active, &.active { + background: #f5f5f5 !important; + border-bottom: 1px solid #f5f5f5 !important; + } + } +} + +.issues-details-filters { + font-size: 13px; + background: #f5f5f5; + margin: -10px 0; + padding: 10px 15px; + margin-top: -15px; + border-left: 1px solid #DDD; + border-right: 1px solid #DDD; + + .btn { + font-size: 13px; + } +} + +@media (min-width: 800px) { + .issues-filters, + .issues_bulk_update { + select, .select2-container { + width: 120px !important; + display: inline-block; + } + } +} + +@media (min-width: 1200px) { + .issues-filters, + .issues_bulk_update { + select, .select2-container { + width: 150px !important; + display: inline-block; + } + } +} + +.issues-filters, +.issues_bulk_update { + .select2-container .select2-choice { + color: #444 !important; + } +} diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss index e8b23090b0f..4282832e2bf 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/generic/forms.scss @@ -15,10 +15,6 @@ input[type='text'].danger { text-shadow: 0 1px 1px #fff } -fieldset legend { - font-size: 16px; -} - .datetime-controls { select { width: 100px; @@ -29,9 +25,14 @@ fieldset legend { padding: 17px 20px 18px; margin-top: 18px; margin-bottom: 18px; - background-color: whitesmoke; - border-top: 1px solid #e5e5e5; - padding-left: 17%; + background-color: $background-color; + border-top: 1px solid $border-color; +} + +@media (min-width: $screen-sm-min) { + .form-actions { + padding-left: 17%; + } } label { @@ -48,14 +49,6 @@ label { width: 250px; } -.input-mx-250 { - max-width: 250px; -} - -.input-mn-300 { - min-width: 300px; -} - .custom-form-control { width: 150px; } @@ -88,139 +81,6 @@ label { @include box-shadow(none); } -.issuable-description { +.wiki-content { margin-top: 35px; } - -.zennable { - position: relative; - - input { - display: none; - } - - .collapse { - display: none; - opacity: 0.5; - - &:before { - content: '\f066'; - font-family: FontAwesome; - color: #000; - font-size: 28px; - position: relative; - padding: 30px 40px 0 0; - } - - &:hover { - opacity: 0.8; - } - } - - .expand { - opacity: 0.5; - - &:before { - content: '\f065'; - font-family: FontAwesome; - color: #000; - font-size: 14px; - line-height: 14px; - padding-right: 20px; - position: relative; - vertical-align: middle; - } - - &:hover { - opacity: 0.8; - } - } - - input:checked ~ .zen-backdrop .expand { - display: none; - } - - input:checked ~ .zen-backdrop .collapse { - display: block; - position: absolute; - top: 0; - } - - label { - position: absolute; - top: -26px; - right: 0; - font-variant: small-caps; - text-transform: uppercase; - font-size: 10px; - padding: 4px; - font-weight: 500; - letter-spacing: 1px; - - &:before { - display: inline-block; - width: 10px; - height: 14px; - } - } - - input:checked ~ .zen-backdrop { - background-color: white; - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 1031; - - textarea { - border: none; - box-shadow: none; - border-radius: 0; - color: #000; - font-size: 20px; - line-height: 26px; - padding: 30px; - display: block; - outline: none; - resize: none; - height: 100vh; - max-width: 900px; - margin: 0 auto; - } - } - - .zen-backdrop textarea::-webkit-input-placeholder { - color: white; - } - - .zen-backdrop textarea:-moz-placeholder { - color: white; - } - - .zen-backdrop textarea::-moz-placeholder { - color: white; - } - - .zen-backdrop textarea:-ms-input-placeholder { - color: white; - } - - input:checked ~ .zen-backdrop textarea::-webkit-input-placeholder { - color: #999; - } - - input:checked ~ .zen-backdrop textarea:-moz-placeholder { - color: #999; - opacity: 1; - } - - input:checked ~ .zen-backdrop textarea::-moz-placeholder { - color: #999; - opacity: 1; - } - - input:checked ~ .zen-backdrop textarea:-ms-input-placeholder { - color: #999; - } -} diff --git a/app/assets/stylesheets/generic/gfm.scss b/app/assets/stylesheets/generic/gfm.scss index e257f053618..8fac5e534fa 100644 --- a/app/assets/stylesheets/generic/gfm.scss +++ b/app/assets/stylesheets/generic/gfm.scss @@ -3,7 +3,8 @@ */ .issue-form, .merge-request-form, .wiki-form { .description { - height: 20em; + height: 16em; + border-top-left-radius: 0; } } @@ -17,4 +18,4 @@ .description { height: 14em; } -}
\ No newline at end of file +} diff --git a/app/assets/stylesheets/generic/header.scss b/app/assets/stylesheets/generic/header.scss new file mode 100644 index 00000000000..26eb7ab1a12 --- /dev/null +++ b/app/assets/stylesheets/generic/header.scss @@ -0,0 +1,221 @@ +/* + * Application Header + * + */ +header { + &.navbar-empty { + background: #FFF; + border-bottom: 1px solid #EEE; + + .center-logo { + margin: 8px 0; + text-align: center; + + img { + height: 32px; + } + } + } + + &.navbar-gitlab { + z-index: 100; + margin-bottom: 0; + min-height: $header-height; + border: none; + width: 100%; + + .container { + background: #FFF; + width: 100% !important; + padding: 0; + filter: none; + + .nav > li > a { + color: #888; + font-size: 14px; + padding: 0; + background-color: #f5f5f5; + margin: ($header-height - 28) / 2 0; + margin-left: 10px; + border-radius: 40px; + height: 28px; + width: 28px; + line-height: 28px; + text-align: center; + + &:hover, &:focus, &:active { + background-color: #EEE; + } + } + + .navbar-toggle { + color: #666; + margin: 0; + border-radius: 0; + position: absolute; + right: 2px; + + &:hover { + background-color: #EEE; + } + } + } + } + + .header-logo { + border-bottom: 1px solid transparent; + float: left; + height: $header-height; + width: $sidebar_width; + + a { + float: left; + height: $header-height; + width: 100%; + padding: ($header-height - 36 ) / 2 8px; + + h3 { + width: 158px; + float: left; + margin: 0; + margin-left: 14px; + font-size: 18px; + line-height: $header-height - 14; + font-weight: normal; + } + + img { + width: 36px; + height: 36px; + float: left; + } + } + + &:hover { + background-color: #EEE; + } + } + + .header-content { + border-bottom: 1px solid #EEE; + padding-right: 35px; + height: $header-height; + + .title { + margin: 0; + padding: 0 15px 0 35px; + overflow: hidden; + font-size: 18px; + line-height: $header-height; + font-weight: bold; + color: #444; + text-overflow: ellipsis; + vertical-align: top; + white-space: nowrap; + + a { + color: #444; + &:hover { + text-decoration: underline; + } + } + } + + .navbar-collapse { + float: right; + } + } + + .search { + margin-right: 10px; + margin-left: 10px; + margin-top: ($header-height - 28) / 2; + + form { + margin: 0; + padding: 0; + } + + .search-input { + width: 220px; + background-image: image-url("icon-search.png"); + background-repeat: no-repeat; + background-position: 10px; + height: inherit; + padding: 4px 6px; + padding-left: 25px; + font-size: 13px; + background-color: #f5f5f5; + border-color: #f5f5f5; + + &:focus { + @include box-shadow(none); + outline: none; + border-color: #DDD; + background-color: #FFF; + } + } + } +} + +@mixin collapsed-header { + .header-logo { + width: $sidebar_collapsed_width; + + h3 { + display: none; + } + } + + .header-content { + .title { + margin-left: 30px; + } + } +} + +@media (max-width: $screen-md-max) { + .header-collapsed, .header-expanded { + @include collapsed-header; + } +} + +@media(min-width: $screen-md-max) { + .header-collapsed { + @include collapsed-header; + } + + .header-expanded { + } +} + +@media (max-width: $screen-xs-max) { + header .container { + font-size: 18px; + + .title { + } + + .navbar-nav { + margin: 0px; + float: none !important; + + .visible-xs, .visable-sm { + display: table-cell !important; + } + } + + .navbar-collapse { + padding-left: 5px; + + li { + display: table-cell; + width: 1%; + + a { + margin-left: 8px !important; + } + } + } + } +} diff --git a/app/assets/stylesheets/generic/highlight.scss b/app/assets/stylesheets/generic/highlight.scss index ae08539d454..2e13ee842e0 100644 --- a/app/assets/stylesheets/generic/highlight.scss +++ b/app/assets/stylesheets/generic/highlight.scss @@ -1,4 +1,4 @@ -.highlighted-data { +.file-content.code { border: none; box-shadow: none; margin: 0px; @@ -10,11 +10,16 @@ border: none; border-radius: 0; font-family: $monospace_font; - font-size: 12px !important; - line-height: 16px !important; + font-size: $code_font_size !important; + line-height: $code_line_height !important; margin: 0; + overflow: auto; + overflow-y: hidden; + white-space: pre; + word-wrap: normal; code { + font-family: $monospace_font; white-space: pre; word-wrap: normal; padding: 0; @@ -25,10 +30,6 @@ } } - .hljs { - padding: 0; - } - .line-numbers { padding: 10px; text-align: right; @@ -37,8 +38,8 @@ a { font-family: $monospace_font; display: block; - font-size: 12px !important; - line-height: 16px !important; + font-size: $code_font_size !important; + line-height: $code_line_height !important; white-space: nowrap; i { @@ -51,18 +52,19 @@ } } } +} - .highlight { - overflow: auto; - overflow-y: hidden; - - pre { - white-space: pre; - word-wrap: normal; +.note-text .code { + border: none; + box-shadow: none; + background: $background-color; + padding: 1em; + overflow-x: auto; - code { - font-family: $monospace_font; - } - } + code { + font-family: $monospace_font; + white-space: pre; + word-wrap: normal; + padding: 0; } } diff --git a/app/assets/stylesheets/generic/issue_box.scss b/app/assets/stylesheets/generic/issue_box.scss index 94149594e24..869e586839b 100644 --- a/app/assets/stylesheets/generic/issue_box.scss +++ b/app/assets/stylesheets/generic/issue_box.scss @@ -1,123 +1,32 @@ /** - * Issue box: - * Huge block (one per page) for storing title, descripion and other information. + * Issue box for showing Open/Closed state: * Used for Issue#show page, MergeRequest#show page etc * - * CLasses: - * .issue-box - Regular box */ .issue-box { - color: #555; - margin:20px 0; - background: $box_bg; - @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); + display: inline-block; + padding: 4px 13px; + font-weight: normal; + margin-right: 5px; &.issue-box-closed { - .state { - background-color: #F3CECE; - border-color: $border_danger; - } - .state-label { - background-color: $bg_danger; - color: #FFF; - } + background-color: $gl-danger; + color: #FFF; } &.issue-box-merged { - .state { - background-color: #B7CEE7; - border-color: $border_primary; - } - .state-label { - background-color: $bg_primary; - color: #FFF; - } + background-color: $gl-primary; + color: #FFF; } &.issue-box-open { - .state { - background-color: #D6F1D7; - border-color: $bg_success; - } - .state-label { - background-color: $bg_success; - color: #FFF; - } + background-color: $gl-success; + color: #FFF; } &.issue-box-expired { - .state { - background-color: #EEE9B3; - border-color: #faebcc; - } - .state-label { - background: #cea61b; - color: #FFF; - } - } - - .control-group { - margin-bottom: 0; - } - - .state { - background-color: #f9f9f9; - } - - .title { - font-size: 28px; - font-weight: normal; - line-height: 1.5; - margin: 0; - color: #333; - padding: 10px 15px; - } - - .context { - border: none; - border-top: 1px solid #eee; - padding: 10px 15px; - - // Reset text align for children - .text-right > * { text-align: left; } - - @media (max-width: $screen-xs-max) { - // Don't right align on mobile - .text-right { text-align: left; } - - .row .col-md-6 { - padding-top: 5px; - } - } - } - - .description { - padding: 0 15px 10px 15px; - - code { - white-space: pre-wrap; - } - } - - .title, .context, .description { - .clearfix { - margin: 0; - } - } - - .state-label { - font-size: 14px; - float: left; - font-weight: bold; - padding: 10px 15px; - } - - .creator { - float: right; - padding: 10px 15px; - a { - text-decoration: underline; - } + background: #cea61b; + color: #FFF; } } diff --git a/app/assets/stylesheets/generic/jquery.scss b/app/assets/stylesheets/generic/jquery.scss index bfbbc7d25e3..871b808bad4 100644 --- a/app/assets/stylesheets/generic/jquery.scss +++ b/app/assets/stylesheets/generic/jquery.scss @@ -41,8 +41,8 @@ } .ui-state-active { - border: 1px solid $bg_primary; - background: $bg_primary; + border: 1px solid $gl-primary; + background: $gl-primary; color: #FFF; } diff --git a/app/assets/stylesheets/generic/lists.scss b/app/assets/stylesheets/generic/lists.scss index 2653bfbf831..c502d953c75 100644 --- a/app/assets/stylesheets/generic/lists.scss +++ b/app/assets/stylesheets/generic/lists.scss @@ -35,18 +35,17 @@ color: #8a6d3b; } - &.smoke { background-color: #f5f5f5; } + &.smoke { background-color: $background-color; } &:hover { background: $hover; - border-bottom: 1px solid darken($hover, 10%); } &:last-child { border-bottom: none; &.bottom { - background: #f5f5f5; + background: $background-color; } } @@ -61,7 +60,7 @@ p { padding-top: 1px; margin: 0; - color: #222; + color: $gray-dark; img { position: relative; top: 3px; @@ -69,15 +68,15 @@ } .well-title { - font-size: 14px; + font-size: $list-font-size; line-height: 18px; } .row_title { - font-weight: 500; - color: #444; + color: $gray-dark; + &:hover { - color: #444; + color: $text-color; text-decoration: underline; } } diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/generic/markdown_area.scss index fbfa72c5e5e..f94677d1925 100644 --- a/app/assets/stylesheets/generic/markdown_area.scss +++ b/app/assets/stylesheets/generic/markdown_area.scss @@ -20,6 +20,7 @@ opacity: 0; font-size: 50px; transition: opacity 200ms ease-in-out; + pointer-events: none; } .div-dropzone-spinner { @@ -50,3 +51,53 @@ margin-bottom: 0; transition: opacity 200ms ease-in-out; } + +.md-area { + position: relative; +} + +.md-header ul { + float: left; +} + +.referenced-users { + padding: 10px 0; + color: #999; + margin-left: 10px; + margin-top: 1px; + margin-right: 130px; +} + +.md-preview-holder { + background: #FFF; + border: 1px solid #ddd; + min-height: 100px; + padding: 5px; + box-shadow: none; +} + +.new_note, +.edit_note, +.issuable-description, +.milestone-description, +.wiki-content, +.merge-request-form { + .nav-tabs { + margin-bottom: 0; + border: none; + + li a, + li.active a { + border: 1px solid #DDD; + } + } +} + +.markdown-area { + background: #FFF; + border: 1px solid #ddd; + min-height: 100px; + padding: 5px; + box-shadow: none; + width: 100%; +} diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss index c164b07b104..a49775daf8b 100644 --- a/app/assets/stylesheets/generic/mobile.scss +++ b/app/assets/stylesheets/generic/mobile.scss @@ -1,9 +1,14 @@ -/** Common mobile (screen XS) styles **/ +/** Common mobile (screen XS, SM) styles **/ @media (max-width: $screen-xs-max) { .container .content { margin-top: 20px; } + .container-fluid { + padding-left: 5px; + padding-right: 5px; + } + .nav.nav-tabs > li > a { padding: 10px; font-size: 12px; @@ -13,5 +18,103 @@ display: none; } } + + .referenced-users { + margin-right: 0; + } + + .issues-filters, + .dash-projects-filters, + .check-all-holder { + display: none; + } + + .rss-btn { + display: none !important; + } + + .project-home-links { + display: none; + } + + .project-avatar { + display: none; + } + + .project-home-panel { + padding-left: 0 !important; + + .project-home-row { + .project-home-desc { + margin-right: 0 !important; + float: none !important; + } + + .project-repo-buttons { + position: static; + margin-top: 15px; + width: 100%; + float: none; + text-align: left; + } + } + } + + .container .title { + padding-left: 15px !important; + } + + .issue-info, .merge-request-info { + display: none; + } + + .issue-details { + .creator, + .page-title .btn-close { + display: none; + } + } + + %ul.notes .note-role, .note-actions { + display: none; + } } +@media (max-width: $screen-sm-max) { + .issues-filters { + .milestone-filter, .labels-filter { + display: none; + } + } + + .page-title { + .note_created_ago, .new-issue-link { + display: none; + } + } + + .issue_edited_ago, .note_edited_ago { + display: none; + } + + aside { + display: none; + } + + .show-aside { + display: block !important; + } +} + +.show-aside { + display: none; + position: fixed; + right: 0px; + top: 30%; + padding: 5px 15px; + background: #EEE; + font-size: 20px; + color: #777; + z-index: 100; + @include box-shadow(0 1px 2px #DDD); +} diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index e0f508d2695..d8e0dc028d1 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -2,25 +2,29 @@ .select2-container, .select2-container.select2-drop-above { .select2-choice { background: #FFF; - border-color: #BBB; - padding: 6px 12px; - font-size: 13px; - line-height: 18px; - height: auto; + border-color: #DDD; + height: 34px; + padding: 6px 14px; + font-size: 14px; + line-height: 1.42857143; + + @include border-radius(4px); .select2-arrow { background: #FFF; - border-left: 1px solid #DDD; + border-left: none; + padding-top: 3px; } } } .select2-container-multi .select2-choices { - @include border-radius(4px) + @include border-radius(4px); + border-color: #CCC; } .select2-container-multi .select2-choices .select2-search-field input { - padding: 6px 12px; + padding: 8px 14px; font-size: 13px; line-height: 18px; height: auto; @@ -29,6 +33,7 @@ .select2-drop-active { border: 1px solid #BBB !important; margin-top: 4px; + font-size: 13px; &.select2-drop-above { margin-bottom: 8px; @@ -42,60 +47,18 @@ .select2-results { max-height: 350px; .select2-highlighted { - background: $bg_primary; - } - } -} - -select { - &.select2 { - width: 100px; - } - - &.select2-sm { - width: 100px; - } -} - -@media (min-width: $screen-sm-min) { - select { - &.select2 { - width: 150px; - } - &.select2-sm { - width: 120px; + background: $gl-primary; } } } -/* Medium devices (desktops, 992px and up) */ -@media (min-width: $screen-md-min) { - select { - &.select2 { - width: 170px; - } - &.select2-sm { - width: 140px; - } - } +.select2-container { + width: 100% !important; } -/* Large devices (large desktops, 1200px and up) */ -@media (min-width: $screen-lg-min) { - select { - &.select2 { - width: 200px; - } - &.select2-sm { - width: 150px; - } - } -} - - /** Branch/tag selector **/ .project-refs-form .select2-container { - margin-right: 10px; + width: 160px !important; } .ajax-users-dropdown, .ajax-project-users-dropdown { @@ -116,6 +79,18 @@ select { } } +.group-result { + .group-image { + float: left; + } + .group-name { + font-weight: bold; + } + .group-path { + color: #999; + } +} + .user-result { .user-image { float: left; @@ -137,3 +112,7 @@ select { font-weight: bolder; } } + +.ajax-users-dropdown { + min-width: 225px !important; +} diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss index f6311ef74e8..65e06e14c73 100644 --- a/app/assets/stylesheets/generic/sidebar.scss +++ b/app/assets/stylesheets/generic/sidebar.scss @@ -1,46 +1,187 @@ -.ui.sidebar { - z-index: 1000 !important; - background: #fff; - padding: 10px; - width: 285px; +.page-with-sidebar { + .sidebar-wrapper { + position: fixed; + top: 0; + left: 0; + height: 100%; + } +} + +.sidebar-wrapper { + z-index: 99; + background: $background-color; +} + +.content-wrapper { + width: 100%; + padding: 15px; + background: #FFF; +} + +.nav-sidebar { + margin: 0; + list-style: none; + + &.navbar-collapse { + padding: 0px !important; + } } -.ui.right.sidebar { - border-left: 1px solid #e1e1e1; - border-right: 0; +.nav-sidebar li a .count { + float: right; + background: #eee; + padding: 0px 8px; + @include border-radius(6px); } -.sidebar-expand-button { - cursor: pointer; - transition: all 0.4s; - -moz-transition: all 0.4s; - -webkit-transition: all 0.4s; +.nav-sidebar li { } -.fixed.sidebar-expand-button { - background: #f9f9f9; - color: #555; - padding: 9px 12px 6px 14px; - border: 1px solid #E1E1E1; - border-right: 0; +.nav-sidebar li { + &.separate-item { + padding-top: 10px; + margin-top: 10px; + } + + a { + color: $gray; + display: block; + text-decoration: none; + padding: 8px 15px; + font-size: 13px; + line-height: 20px; + padding-left: 16px; + + &:hover { + text-decoration: none; + } + + &:active, &:focus { + text-decoration: none; + } + + i { + width: 20px; + color: $gray-light; + margin-right: 23px; + } + } +} + +.sidebar-subnav { + margin-left: 0px; + padding-left: 0px; + + li { + list-style: none; + } +} + +@mixin expanded-sidebar { + padding-left: $sidebar_width; + + .sidebar-wrapper { + width: $sidebar_width; + + .nav-sidebar { + margin-top: 29px; + position: fixed; + top: $header-height; + width: $sidebar_width; + } + } + + .content-wrapper { + padding: 20px; + } +} + +@mixin folded-sidebar { + padding-left: 50px; + + .sidebar-wrapper { + width: $sidebar_collapsed_width; + + .nav-sidebar { + margin-top: 29px; + position: fixed; + top: $header-height; + width: $sidebar_collapsed_width; + + li a { + padding-left: 18px; + font-size: 14px; + padding: 8px 15px; + text-align: center; + + + & > span { + display: none; + } + } + } + + .collapse-nav a { + left: 0px; + width: $sidebar_collapsed_width; + } + + .sidebar-user { + .username { + display: none; + } + } + } +} + +.collapse-nav a { position: fixed; - top: 108px; - right: 0px; - margin-right: 0; - &:hover { - background: #ddd; - color: #333; - padding-right: 25px; + top: $header-height; + left: 198px; + font-size: 13px; + background: transparent; + width: 32px; + height: 28px; + text-align: center; + line-height: 28px; +} + +.collapse-nav a:hover { + text-decoration: none; + background: #f2f6f7; +} + +@media (max-width: $screen-md-max) { + .page-sidebar-collapsed { + @include folded-sidebar; + } + + .page-sidebar-expanded { + @include folded-sidebar; + } + + .collapse-nav { + display: none; } } -.btn.btn-default.sidebar-expand-button { - margin-left: 12px; - display: inline-block !important; +@media(min-width: $screen-md-max) { + .page-sidebar-collapsed { + @include folded-sidebar; + } + + .page-sidebar-expanded { + @include expanded-sidebar; + } } -@media (min-width: 767px) { -.btn.btn-default.sidebar-expand-button { - display: none!important; - } +.sidebar-user { + position: absolute; + bottom: 0; + width: 100%; + padding: 10px; + + .username { + margin-top: 5px; + } } diff --git a/app/assets/stylesheets/generic/tables.scss b/app/assets/stylesheets/generic/tables.scss new file mode 100644 index 00000000000..a66e45577de --- /dev/null +++ b/app/assets/stylesheets/generic/tables.scss @@ -0,0 +1,20 @@ +table { + &.table { + tr { + td, th { + padding: 8px 10px; + line-height: 20px; + vertical-align: middle; + } + th { + font-weight: normal; + font-size: 15px; + border-bottom: 1px solid $border-color !important; + } + td { + border-color: #F1F1F1 !important; + border-bottom: 1px solid; + } + } + } +} diff --git a/app/assets/stylesheets/generic/timeline.scss b/app/assets/stylesheets/generic/timeline.scss index 57e9e8ae5c5..97831eb7c27 100644 --- a/app/assets/stylesheets/generic/timeline.scss +++ b/app/assets/stylesheets/generic/timeline.scss @@ -42,7 +42,7 @@ background: #fff; color: #737881; float: left; - @include border-radius(40px); + @include border-radius($avatar_radius); @include box-shadow(0 0 0 3px #EEE); overflow: hidden; @@ -54,10 +54,14 @@ .timeline-content { position: relative; - background: #f5f5f6; + background: $background-color; padding: 10px 15px; margin-left: 60px; + img { + max-width: 100%; + } + &:after { content: ''; display: block; @@ -66,7 +70,7 @@ height: 0; border-style: solid; border-width: 9px 9px 9px 0; - border-color: transparent #f5f5f6 transparent transparent; + border-color: transparent $background-color transparent transparent; left: 0; top: 10px; margin-left: -9px; @@ -74,6 +78,42 @@ } } } + + .system-note .timeline-entry-inner { + .timeline-icon { + background: none; + margin-left: 12px; + margin-top: 0; + @include box-shadow(none); + + span { + margin: 0 2px; + font-size: 16px; + color: #eeeeee; + } + } + + .timeline-content { + background: none; + margin-left: 45px; + padding: 0px 15px; + + &:after { border: 0; } + + .note-header { + span { font-size: 12px; } + + .avatar { + margin-right: 5px; + } + } + + .note-text { + font-size: 12px; + margin-left: 20px; + } + } + } } @media (max-width: $screen-xs-max) { diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index 385a627b4be..66767cb13cb 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -2,24 +2,11 @@ * Headers * */ -h1.page-title { - @include page-title; - font-size: 28px; -} - -h2.page-title { - @include page-title; - font-size: 24px; -} - -h3.page-title { - @include page-title; - font-size: 22px; -} - -h6 { - color: #888; - text-transform: uppercase; +.page-title { + margin-top: 0px; + line-height: 1.5; + font-weight: normal; + margin-bottom: 5px; } /** CODE **/ @@ -28,60 +15,21 @@ pre { &.dark { background: #333; - color: #f5f5f5; - } -} - -/** - * Links - * - */ -a { - outline: none; - color: $link_color; - &:hover { - text-decoration: underline; - color: $link_hover_color; - } - - &:focus { - text-decoration: underline; - } - - &.darken { - color: $style_color; - } - - &.lined { - text-decoration: underline; - &:hover { text-decoration: underline; } - } - - &.gray { - color: gray; - } - - &.supp_diff_link { - text-align: center; - padding: 20px 0; - background: #f1f1f1; - width: 100%; - float: left; + color: $background-color; } - - &.neib { - margin-right: 15px; - } -} - -a:focus { - outline: none; } .monospace { font-family: $monospace_font; } +code { + &.key-fingerprint { + background: $body-bg; + color: $text-color; + } +} + /** * Wiki typography * @@ -94,7 +42,14 @@ a:focus { /* Link to current header. */ h1, h2, h3, h4, h5, h6 { position: relative; - &:hover > :last-child { + + a.anchor { + // Setting `display: none` would prevent the anchor being scrolled to, so + // instead we set the height to 0 and it gets updated on hover. + height: 0; + } + + &:hover > a.anchor { $size: 16px; position: absolute; right: 100%; @@ -128,3 +83,7 @@ a:focus { textarea.js-gfm-input { font-family: $monospace_font; } + +.strikethrough { + text-decoration: line-through; +} diff --git a/app/assets/stylesheets/generic/zen.scss b/app/assets/stylesheets/generic/zen.scss new file mode 100644 index 00000000000..7ab01187a02 --- /dev/null +++ b/app/assets/stylesheets/generic/zen.scss @@ -0,0 +1,105 @@ +.zennable { + .zen-toggle-comment { + display: none; + } + + .zen-enter-link { + color: #888; + position: absolute; + top: 0px; + right: 4px; + line-height: 40px; + } + + .zen-leave-link { + display: none; + color: #888; + position: absolute; + top: 10px; + right: 10px; + padding: 5px; + font-size: 36px; + + &:hover { + color: #111; + } + } + + // Hide the Enter link when we're in Zen mode + input:checked ~ .zen-backdrop .zen-enter-link { + display: none; + } + + // Show the Leave link when we're in Zen mode + input:checked ~ .zen-backdrop .zen-leave-link { + display: block; + position: absolute; + top: 0; + } + + input:checked ~ .zen-backdrop { + background-color: white; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 1031; + + textarea { + border: none; + box-shadow: none; + border-radius: 0; + color: #000; + font-size: 20px; + line-height: 26px; + padding: 30px; + display: block; + outline: none; + resize: none; + height: 100vh; + max-width: 900px; + margin: 0 auto; + } + } + + // Make the placeholder text in the standard textarea the same color as the + // background, effectively hiding it + + .zen-backdrop textarea::-webkit-input-placeholder { + color: white; + } + + .zen-backdrop textarea:-moz-placeholder { + color: white; + } + + .zen-backdrop textarea::-moz-placeholder { + color: white; + } + + .zen-backdrop textarea:-ms-input-placeholder { + color: white; + } + + // Make the color of the placeholder text in the Zenned-out textarea darker, + // so it becomes visible + + input:checked ~ .zen-backdrop textarea::-webkit-input-placeholder { + color: #999; + } + + input:checked ~ .zen-backdrop textarea:-moz-placeholder { + color: #999; + opacity: 1; + } + + input:checked ~ .zen-backdrop textarea::-moz-placeholder { + color: #999; + opacity: 1; + } + + input:checked ~ .zen-backdrop textarea:-ms-input-placeholder { + color: #999; + } +} diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index ca51da3fdd4..c8cb18ec35f 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -1,199 +1,88 @@ -.dark { - background-color: #232323; +/* https://github.com/MozMorris/tomorrow-pygments */ +pre.code.highlight.dark, +.code.dark { - .line.hll { - background: #558; - } - - .highlight{ - border-left: 1px solid #444; - } - - .no-highlight { - color: #DDD; - } + background-color: #1d1f21; + color: #c5c8c6; + pre.code, + .line-numbers, .line-numbers a { - color: #666; - } - - pre { - background-color: #232323; - } - - .hljs { - display: block; - background: #232323; - color: #E6E1DC; - } - - .hljs-comment, - .hljs-template_comment, - .hljs-javadoc, - .hljs-shebang { - color: #BC9458; - font-style: italic; - } - - .hljs-keyword, - .ruby .hljs-function .hljs-keyword, - .hljs-request, - .hljs-status, - .nginx .hljs-title, - .method, - .hljs-list .hljs-title { - color: #C26230; - } - - .hljs-string, - .hljs-number, - .hljs-regexp, - .hljs-tag .hljs-value, - .hljs-cdata, - .hljs-filter .hljs-argument, - .hljs-attr_selector, - .apache .hljs-cbracket, - .hljs-date, - .tex .hljs-command, - .markdown .hljs-link_label { - color: #A5C261; - } - - .hljs-subst { - color: #519F50; - } - - .hljs-tag, - .hljs-tag .hljs-keyword, - .hljs-tag .hljs-title, - .hljs-doctype, - .hljs-sub .hljs-identifier, - .hljs-pi, - .input_number { - color: #E8BF6A; - } - - .hljs-identifier { - color: #D0D0FF; - } - - .hljs-class .hljs-title, - .haskell .hljs-type, - .smalltalk .hljs-class, - .hljs-javadoctag, - .hljs-yardoctag, - .hljs-phpdoc { - text-decoration: none; - } - - .hljs-constant { - color: #DA4939; - } - - - .hljs-symbol, - .hljs-built_in, - .ruby .hljs-symbol .hljs-string, - .ruby .hljs-symbol .hljs-identifier, - .markdown .hljs-link_url, - .hljs-attribute { - color: #6D9CBE; - } - - .markdown .hljs-link_url { - text-decoration: underline; - } - - - - .hljs-params, - .hljs-variable, - .clojure .hljs-attribute { - color: #D0D0FF; - } - - .css .hljs-tag, - .hljs-rules .hljs-property, - .hljs-pseudo, - .tex .hljs-special { - color: #CDA869; - } - - .css .hljs-class { - color: #9B703F; - } - - .hljs-rules .hljs-keyword { - color: #C5AF75; - } - - .hljs-rules .hljs-value { - color: #CF6A4C; - } - - .css .hljs-id { - color: #8B98AB; - } - - .hljs-annotation, - .apache .hljs-sqbracket, - .nginx .hljs-built_in { - color: #9B859D; - } - - .hljs-preprocessor, - .hljs-preprocessor *, - .hljs-pragma { - color: #8996A8 !important; - } - - .hljs-hexcolor, - .css .hljs-value .hljs-number { - color: #A5C261; - } - - .hljs-title, - .hljs-decorator, - .css .hljs-function { - color: #FFC66D; - } - - .diff .hljs-header, - .hljs-chunk { - background-color: #2F33AB; - color: #E6E1DC; - display: inline-block; - width: 100%; - } - - .diff .hljs-change { - background-color: #4A410D; - color: #F8F8F8; - display: inline-block; - width: 100%; - } - - .hljs-addition { - background-color: #144212; - color: #E6E1DC; - display: inline-block; - width: 100%; - } - - .hljs-deletion { - background-color: #600; - color: #E6E1DC; - display: inline-block; - width: 100%; - } - - .coffeescript .javascript, - .javascript .xml, - .tex .hljs-formula, - .xml .javascript, - .xml .vbscript, - .xml .css, - .xml .hljs-cdata { - opacity: 0.7; - } + background-color: #1d1f21 !important; + color: #c5c8c6 !important; + } + + pre.code { + border-left: 1px solid #666; + } + + // highlight line via anchor + pre .hll { + background-color: #557 !important; + } + + .hll { background-color: #373b41 } + .c { color: #969896 } /* Comment */ + .err { color: #cc6666 } /* Error */ + .k { color: #b294bb } /* Keyword */ + .l { color: #de935f } /* Literal */ + .n { color: #c5c8c6 } /* Name */ + .o { color: #8abeb7 } /* Operator */ + .p { color: #c5c8c6 } /* Punctuation */ + .cm { color: #969896 } /* Comment.Multiline */ + .cp { color: #969896 } /* Comment.Preproc */ + .c1 { color: #969896 } /* Comment.Single */ + .cs { color: #969896 } /* Comment.Special */ + .gd { color: #cc6666 } /* Generic.Deleted */ + .ge { font-style: italic } /* Generic.Emph */ + .gh { color: #c5c8c6; font-weight: bold } /* Generic.Heading */ + .gi { color: #b5bd68 } /* Generic.Inserted */ + .gp { color: #969896; font-weight: bold } /* Generic.Prompt */ + .gs { font-weight: bold } /* Generic.Strong */ + .gu { color: #8abeb7; font-weight: bold } /* Generic.Subheading */ + .kc { color: #b294bb } /* Keyword.Constant */ + .kd { color: #b294bb } /* Keyword.Declaration */ + .kn { color: #8abeb7 } /* Keyword.Namespace */ + .kp { color: #b294bb } /* Keyword.Pseudo */ + .kr { color: #b294bb } /* Keyword.Reserved */ + .kt { color: #f0c674 } /* Keyword.Type */ + .ld { color: #b5bd68 } /* Literal.Date */ + .m { color: #de935f } /* Literal.Number */ + .s { color: #b5bd68 } /* Literal.String */ + .na { color: #81a2be } /* Name.Attribute */ + .nb { color: #c5c8c6 } /* Name.Builtin */ + .nc { color: #f0c674 } /* Name.Class */ + .no { color: #cc6666 } /* Name.Constant */ + .nd { color: #8abeb7 } /* Name.Decorator */ + .ni { color: #c5c8c6 } /* Name.Entity */ + .ne { color: #cc6666 } /* Name.Exception */ + .nf { color: #81a2be } /* Name.Function */ + .nl { color: #c5c8c6 } /* Name.Label */ + .nn { color: #f0c674 } /* Name.Namespace */ + .nx { color: #81a2be } /* Name.Other */ + .py { color: #c5c8c6 } /* Name.Property */ + .nt { color: #8abeb7 } /* Name.Tag */ + .nv { color: #cc6666 } /* Name.Variable */ + .ow { color: #8abeb7 } /* Operator.Word */ + .w { color: #c5c8c6 } /* Text.Whitespace */ + .mf { color: #de935f } /* Literal.Number.Float */ + .mh { color: #de935f } /* Literal.Number.Hex */ + .mi { color: #de935f } /* Literal.Number.Integer */ + .mo { color: #de935f } /* Literal.Number.Oct */ + .sb { color: #b5bd68 } /* Literal.String.Backtick */ + .sc { color: #c5c8c6 } /* Literal.String.Char */ + .sd { color: #969896 } /* Literal.String.Doc */ + .s2 { color: #b5bd68 } /* Literal.String.Double */ + .se { color: #de935f } /* Literal.String.Escape */ + .sh { color: #b5bd68 } /* Literal.String.Heredoc */ + .si { color: #de935f } /* Literal.String.Interpol */ + .sx { color: #b5bd68 } /* Literal.String.Other */ + .sr { color: #b5bd68 } /* Literal.String.Regex */ + .s1 { color: #b5bd68 } /* Literal.String.Single */ + .ss { color: #b5bd68 } /* Literal.String.Symbol */ + .bp { color: #c5c8c6 } /* Name.Builtin.Pseudo */ + .vc { color: #cc6666 } /* Name.Variable.Class */ + .vg { color: #cc6666 } /* Name.Variable.Global */ + .vi { color: #cc6666 } /* Name.Variable.Instance */ + .il { color: #de935f } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 36bc5df2f44..001e8b31020 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -1,148 +1,88 @@ -.monokai { - background-color: #272822; +/* https://github.com/richleland/pygments-css/blob/master/monokai.css */ +pre.code.monokai, +.code.monokai { - .highlight{ - border-left: 1px solid #444; - } - - .line.hll { - background: #558; - } - - .no-highlight { - color: #DDD; - } + background: #272822; + color: #f8f8f2; + pre.highlight, + .line-numbers, .line-numbers a { - color: #666; + background:#272822 !important; + color:#f8f8f2 !important; } - pre { - background-color: #272822; - color: #f8f8f2; + pre.code { + border-left: 1px solid #555; } - .hljs { - display: block; - background: #272822; + // highlight line via anchor + pre .hll { + background-color: #49483e !important; } - .hljs-tag, - .hljs-tag .hljs-title, - .hljs-keyword, - .hljs-literal, - .hljs-strong, - .hljs-change, - .hljs-winutils, - .hljs-flow, - .lisp .hljs-title, - .clojure .hljs-built_in, - .nginx .hljs-title, - .tex .hljs-special { - color: #F92672; - } + .hll { background-color: #49483e } + .c { color: #75715e } /* Comment */ + .err { color: #960050; background-color: #1e0010 } /* Error */ + .k { color: #66d9ef } /* Keyword */ + .l { color: #ae81ff } /* Literal */ + .n { color: #f8f8f2 } /* Name */ + .o { color: #f92672 } /* Operator */ + .p { color: #f8f8f2 } /* Punctuation */ + .cm { color: #75715e } /* Comment.Multiline */ + .cp { color: #75715e } /* Comment.Preproc */ + .c1 { color: #75715e } /* Comment.Single */ + .cs { color: #75715e } /* Comment.Special */ + .ge { font-style: italic } /* Generic.Emph */ + .gs { font-weight: bold } /* Generic.Strong */ + .kc { color: #66d9ef } /* Keyword.Constant */ + .kd { color: #66d9ef } /* Keyword.Declaration */ + .kn { color: #f92672 } /* Keyword.Namespace */ + .kp { color: #66d9ef } /* Keyword.Pseudo */ + .kr { color: #66d9ef } /* Keyword.Reserved */ + .kt { color: #66d9ef } /* Keyword.Type */ + .ld { color: #e6db74 } /* Literal.Date */ + .m { color: #ae81ff } /* Literal.Number */ + .s { color: #e6db74 } /* Literal.String */ + .na { color: #a6e22e } /* Name.Attribute */ + .nb { color: #f8f8f2 } /* Name.Builtin */ + .nc { color: #a6e22e } /* Name.Class */ + .no { color: #66d9ef } /* Name.Constant */ + .nd { color: #a6e22e } /* Name.Decorator */ + .ni { color: #f8f8f2 } /* Name.Entity */ + .ne { color: #a6e22e } /* Name.Exception */ + .nf { color: #a6e22e } /* Name.Function */ + .nl { color: #f8f8f2 } /* Name.Label */ + .nn { color: #f8f8f2 } /* Name.Namespace */ + .nx { color: #a6e22e } /* Name.Other */ + .py { color: #f8f8f2 } /* Name.Property */ + .nt { color: #f92672 } /* Name.Tag */ + .nv { color: #f8f8f2 } /* Name.Variable */ + .ow { color: #f92672 } /* Operator.Word */ + .w { color: #f8f8f2 } /* Text.Whitespace */ + .mf { color: #ae81ff } /* Literal.Number.Float */ + .mh { color: #ae81ff } /* Literal.Number.Hex */ + .mi { color: #ae81ff } /* Literal.Number.Integer */ + .mo { color: #ae81ff } /* Literal.Number.Oct */ + .sb { color: #e6db74 } /* Literal.String.Backtick */ + .sc { color: #e6db74 } /* Literal.String.Char */ + .sd { color: #e6db74 } /* Literal.String.Doc */ + .s2 { color: #e6db74 } /* Literal.String.Double */ + .se { color: #ae81ff } /* Literal.String.Escape */ + .sh { color: #e6db74 } /* Literal.String.Heredoc */ + .si { color: #e6db74 } /* Literal.String.Interpol */ + .sx { color: #e6db74 } /* Literal.String.Other */ + .sr { color: #e6db74 } /* Literal.String.Regex */ + .s1 { color: #e6db74 } /* Literal.String.Single */ + .ss { color: #e6db74 } /* Literal.String.Symbol */ + .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ + .vc { color: #f8f8f2 } /* Name.Variable.Class */ + .vg { color: #f8f8f2 } /* Name.Variable.Global */ + .vi { color: #f8f8f2 } /* Name.Variable.Instance */ + .il { color: #ae81ff } /* Literal.Number.Integer.Long */ - .hljs { - color: #DDD; - } - - .hljs .hljs-constant, - .asciidoc .hljs-code { - color: #66D9EF; - } - - .hljs-code, - .hljs-class .hljs-title, - .hljs-header { - color: white; - } - - .hljs-link_label, - .hljs-attribute, - .hljs-symbol, - .hljs-symbol .hljs-string, - .hljs-value, - .hljs-regexp { - color: #BF79DB; - } - - .hljs-link_url, - .hljs-tag .hljs-value, - .hljs-string, - .hljs-bullet, - .hljs-subst, - .hljs-title, - .hljs-emphasis, - .haskell .hljs-type, - .hljs-preprocessor, - .hljs-pragma, - .ruby .hljs-class .hljs-parent, - .hljs-built_in, - .sql .hljs-aggregate, - .django .hljs-template_tag, - .django .hljs-variable, - .smalltalk .hljs-class, - .hljs-javadoc, - .django .hljs-filter .hljs-argument, - .smalltalk .hljs-localvars, - .smalltalk .hljs-array, - .hljs-attr_selector, - .hljs-pseudo, - .hljs-addition, - .hljs-stream, - .hljs-envvar, - .apache .hljs-tag, - .apache .hljs-cbracket, - .tex .hljs-command, - .hljs-prompt { - color: #A6E22E; - } - - .hljs-comment, - .java .hljs-annotation, - .smartquote, - .hljs-blockquote, - .hljs-horizontal_rule, - .python .hljs-decorator, - .hljs-template_comment, - .hljs-pi, - .hljs-doctype, - .hljs-deletion, - .hljs-shebang, - .apache .hljs-sqbracket, - .tex .hljs-formula { - color: #75715E; - } - - .hljs-keyword, - .hljs-literal, - .css .hljs-id, - .hljs-phpdoc, - .hljs-title, - .hljs-header, - .haskell .hljs-type, - .vbscript .hljs-built_in, - .sql .hljs-aggregate, - .rsl .hljs-built_in, - .smalltalk .hljs-class, - .diff .hljs-header, - .hljs-chunk, - .hljs-winutils, - .bash .hljs-variable, - .apache .hljs-tag, - .tex .hljs-special, - .hljs-request, - .hljs-status { - font-weight: bold; - } - - .coffeescript .javascript, - .javascript .xml, - .tex .hljs-formula, - .xml .javascript, - .xml .vbscript, - .xml .css, - .xml .hljs-cdata { - opacity: 0.5; - } + .gh { } /* Generic Heading & Diff Header */ + .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */ + .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */ + .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */ } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index b9bec225188..f5b827e7c02 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -1,125 +1,110 @@ -.solarized-dark { - background-color: #002B36; +/* https://gist.github.com/qguv/7936275 */ +pre.code.highlight.solarized-dark, +.code.solarized-dark { - .highlight{ - border-left: 1px solid #113b46; - } - - .line.hll { - background: #000; - } - - .no-highlight { - color: #DDD; - } - - pre { - background-color: #002B36; - color: #eee; - } + background-color: #002b36; + color: #93a1a1; + pre.code, + .line-numbers, .line-numbers a { - color: #666; - } - - .hljs { - display: block; - background: #002b36; - color: #839496; + background-color: #002b36 !important; + color: #93a1a1 !important; } - .hljs-comment, - .hljs-template_comment, - .diff .hljs-header, - .hljs-doctype, - .hljs-pi, - .lisp .hljs-string, - .hljs-javadoc { - color: #586e75; - } - - /* Solarized Green */ - .hljs-keyword, - .hljs-winutils, - .method, - .hljs-addition, - .css .hljs-tag, - .hljs-request, - .hljs-status, - .nginx .hljs-title { - color: #859900; - } - - /* Solarized Cyan */ - .hljs-number, - .hljs-command, - .hljs-string, - .hljs-tag .hljs-value, - .hljs-rules .hljs-value, - .hljs-phpdoc, - .tex .hljs-formula, - .hljs-regexp, - .hljs-hexcolor, - .hljs-link_url { - color: #2aa198; + pre.code { + border-left: 1px solid #113b46; } - /* Solarized Blue */ - .hljs-title, - .hljs-localvars, - .hljs-chunk, - .hljs-decorator, - .hljs-built_in, - .hljs-identifier, - .vhdl .hljs-literal, - .hljs-id, - .css .hljs-function { - color: #268bd2; + // highlight line via anchor + pre .hll { + background-color: #174652 !important; } - /* Solarized Yellow */ - .hljs-attribute, - .hljs-variable, - .lisp .hljs-body, - .smalltalk .hljs-number, - .hljs-constant, - .hljs-class .hljs-title, - .hljs-parent, - .haskell .hljs-type, - .hljs-link_reference { - color: #b58900; - } + /* Solarized Dark - /* Solarized Orange */ - .hljs-preprocessor, - .hljs-preprocessor .hljs-keyword, - .hljs-pragma, - .hljs-shebang, - .hljs-symbol, - .hljs-symbol .hljs-string, - .diff .hljs-change, - .hljs-special, - .hljs-attr_selector, - .hljs-subst, - .hljs-cdata, - .clojure .hljs-title, - .css .hljs-pseudo, - .hljs-header { - color: #cb4b16; - } + For use with Jekyll and Pygments - /* Solarized Red */ - .hljs-deletion, - .hljs-important { - color: #dc322f; - } + http://ethanschoonover.com/solarized - /* Solarized Violet */ - .hljs-link_label { - color: #6c71c4; - } + SOLARIZED HEX ROLE + --------- -------- ------------------------------------------ + base03 #002b36 background + base01 #586e75 comments / secondary content + base1 #93a1a1 body text / default code / primary content + orange #cb4b16 constants + red #dc322f regex, special keywords + blue #268bd2 reserved keywords + cyan #2aa198 strings, numbers + green #859900 operators, other keywords + */ - .tex .hljs-formula { - background: #073642; - } + .c { color: #586e75 } /* Comment */ + .err { color: #93a1a1 } /* Error */ + .g { color: #93a1a1 } /* Generic */ + .k { color: #859900 } /* Keyword */ + .l { color: #93a1a1 } /* Literal */ + .n { color: #93a1a1 } /* Name */ + .o { color: #859900 } /* Operator */ + .x { color: #cb4b16 } /* Other */ + .p { color: #93a1a1 } /* Punctuation */ + .cm { color: #586e75 } /* Comment.Multiline */ + .cp { color: #859900 } /* Comment.Preproc */ + .c1 { color: #586e75 } /* Comment.Single */ + .cs { color: #859900 } /* Comment.Special */ + .gd { color: #2aa198 } /* Generic.Deleted */ + .ge { color: #93a1a1; font-style: italic } /* Generic.Emph */ + .gr { color: #dc322f } /* Generic.Error */ + .gh { color: #cb4b16 } /* Generic.Heading */ + .gi { color: #859900 } /* Generic.Inserted */ + .go { color: #93a1a1 } /* Generic.Output */ + .gp { color: #93a1a1 } /* Generic.Prompt */ + .gs { color: #93a1a1; font-weight: bold } /* Generic.Strong */ + .gu { color: #cb4b16 } /* Generic.Subheading */ + .gt { color: #93a1a1 } /* Generic.Traceback */ + .kc { color: #cb4b16 } /* Keyword.Constant */ + .kd { color: #268bd2 } /* Keyword.Declaration */ + .kn { color: #859900 } /* Keyword.Namespace */ + .kp { color: #859900 } /* Keyword.Pseudo */ + .kr { color: #268bd2 } /* Keyword.Reserved */ + .kt { color: #dc322f } /* Keyword.Type */ + .ld { color: #93a1a1 } /* Literal.Date */ + .m { color: #2aa198 } /* Literal.Number */ + .s { color: #2aa198 } /* Literal.String */ + .na { color: #93a1a1 } /* Name.Attribute */ + .nb { color: #B58900 } /* Name.Builtin */ + .nc { color: #268bd2 } /* Name.Class */ + .no { color: #cb4b16 } /* Name.Constant */ + .nd { color: #268bd2 } /* Name.Decorator */ + .ni { color: #cb4b16 } /* Name.Entity */ + .ne { color: #cb4b16 } /* Name.Exception */ + .nf { color: #268bd2 } /* Name.Function */ + .nl { color: #93a1a1 } /* Name.Label */ + .nn { color: #93a1a1 } /* Name.Namespace */ + .nx { color: #93a1a1 } /* Name.Other */ + .py { color: #93a1a1 } /* Name.Property */ + .nt { color: #268bd2 } /* Name.Tag */ + .nv { color: #268bd2 } /* Name.Variable */ + .ow { color: #859900 } /* Operator.Word */ + .w { color: #93a1a1 } /* Text.Whitespace */ + .mf { color: #2aa198 } /* Literal.Number.Float */ + .mh { color: #2aa198 } /* Literal.Number.Hex */ + .mi { color: #2aa198 } /* Literal.Number.Integer */ + .mo { color: #2aa198 } /* Literal.Number.Oct */ + .sb { color: #586e75 } /* Literal.String.Backtick */ + .sc { color: #2aa198 } /* Literal.String.Char */ + .sd { color: #93a1a1 } /* Literal.String.Doc */ + .s2 { color: #2aa198 } /* Literal.String.Double */ + .se { color: #cb4b16 } /* Literal.String.Escape */ + .sh { color: #93a1a1 } /* Literal.String.Heredoc */ + .si { color: #2aa198 } /* Literal.String.Interpol */ + .sx { color: #2aa198 } /* Literal.String.Other */ + .sr { color: #dc322f } /* Literal.String.Regex */ + .s1 { color: #2aa198 } /* Literal.String.Single */ + .ss { color: #2aa198 } /* Literal.String.Symbol */ + .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ + .vc { color: #268bd2 } /* Name.Variable.Class */ + .vg { color: #268bd2 } /* Name.Variable.Global */ + .vi { color: #268bd2 } /* Name.Variable.Instance */ + .il { color: #2aa198 } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss new file mode 100644 index 00000000000..6b44c00c305 --- /dev/null +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -0,0 +1,110 @@ +/* https://gist.github.com/qguv/7936275 */ +pre.code.highlight.solarized-light, +.code.solarized-light { + + background-color: #fdf6e3; + color: #586e75; + + pre.code, + .line-numbers, + .line-numbers a { + background-color: #fdf6e3 !important; + color: #586e75 !important; + } + + pre.code { + border-left: 1px solid #c5d0d4; + } + + // highlight line via anchor + pre .hll { + background-color: #ddd8c5 !important; + } + + /* Solarized Light + + For use with Jekyll and Pygments + + http://ethanschoonover.com/solarized + + SOLARIZED HEX ROLE + --------- -------- ------------------------------------------ + base01 #586e75 body text / default code / primary content + base1 #93a1a1 comments / secondary content + base3 #fdf6e3 background + orange #cb4b16 constants + red #dc322f regex, special keywords + blue #268bd2 reserved keywords + cyan #2aa198 strings, numbers + green #859900 operators, other keywords + */ + + .c { color: #93a1a1 } /* Comment */ + .err { color: #586e75 } /* Error */ + .g { color: #586e75 } /* Generic */ + .k { color: #859900 } /* Keyword */ + .l { color: #586e75 } /* Literal */ + .n { color: #586e75 } /* Name */ + .o { color: #859900 } /* Operator */ + .x { color: #cb4b16 } /* Other */ + .p { color: #586e75 } /* Punctuation */ + .cm { color: #93a1a1 } /* Comment.Multiline */ + .cp { color: #859900 } /* Comment.Preproc */ + .c1 { color: #93a1a1 } /* Comment.Single */ + .cs { color: #859900 } /* Comment.Special */ + .gd { color: #2aa198 } /* Generic.Deleted */ + .ge { color: #586e75; font-style: italic } /* Generic.Emph */ + .gr { color: #dc322f } /* Generic.Error */ + .gh { color: #cb4b16 } /* Generic.Heading */ + .gi { color: #859900 } /* Generic.Inserted */ + .go { color: #586e75 } /* Generic.Output */ + .gp { color: #586e75 } /* Generic.Prompt */ + .gs { color: #586e75; font-weight: bold } /* Generic.Strong */ + .gu { color: #cb4b16 } /* Generic.Subheading */ + .gt { color: #586e75 } /* Generic.Traceback */ + .kc { color: #cb4b16 } /* Keyword.Constant */ + .kd { color: #268bd2 } /* Keyword.Declaration */ + .kn { color: #859900 } /* Keyword.Namespace */ + .kp { color: #859900 } /* Keyword.Pseudo */ + .kr { color: #268bd2 } /* Keyword.Reserved */ + .kt { color: #dc322f } /* Keyword.Type */ + .ld { color: #586e75 } /* Literal.Date */ + .m { color: #2aa198 } /* Literal.Number */ + .s { color: #2aa198 } /* Literal.String */ + .na { color: #586e75 } /* Name.Attribute */ + .nb { color: #B58900 } /* Name.Builtin */ + .nc { color: #268bd2 } /* Name.Class */ + .no { color: #cb4b16 } /* Name.Constant */ + .nd { color: #268bd2 } /* Name.Decorator */ + .ni { color: #cb4b16 } /* Name.Entity */ + .ne { color: #cb4b16 } /* Name.Exception */ + .nf { color: #268bd2 } /* Name.Function */ + .nl { color: #586e75 } /* Name.Label */ + .nn { color: #586e75 } /* Name.Namespace */ + .nx { color: #586e75 } /* Name.Other */ + .py { color: #586e75 } /* Name.Property */ + .nt { color: #268bd2 } /* Name.Tag */ + .nv { color: #268bd2 } /* Name.Variable */ + .ow { color: #859900 } /* Operator.Word */ + .w { color: #586e75 } /* Text.Whitespace */ + .mf { color: #2aa198 } /* Literal.Number.Float */ + .mh { color: #2aa198 } /* Literal.Number.Hex */ + .mi { color: #2aa198 } /* Literal.Number.Integer */ + .mo { color: #2aa198 } /* Literal.Number.Oct */ + .sb { color: #93a1a1 } /* Literal.String.Backtick */ + .sc { color: #2aa198 } /* Literal.String.Char */ + .sd { color: #586e75 } /* Literal.String.Doc */ + .s2 { color: #2aa198 } /* Literal.String.Double */ + .se { color: #cb4b16 } /* Literal.String.Escape */ + .sh { color: #586e75 } /* Literal.String.Heredoc */ + .si { color: #2aa198 } /* Literal.String.Interpol */ + .sx { color: #2aa198 } /* Literal.String.Other */ + .sr { color: #dc322f } /* Literal.String.Regex */ + .s1 { color: #2aa198 } /* Literal.String.Single */ + .ss { color: #2aa198 } /* Literal.String.Symbol */ + .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ + .vc { color: #268bd2 } /* Name.Variable.Class */ + .vg { color: #268bd2 } /* Name.Variable.Global */ + .vi { color: #268bd2 } /* Name.Variable.Instance */ + .il { color: #2aa198 } /* Literal.Number.Integer.Long */ +} diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 8d5822937a0..a52ffc971d1 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -1,196 +1,87 @@ -.white { - .line.hll { - background: #FFA; - } - - pre { - background-color: #fff; - color: #333; - } +/* https://github.com/aahan/pygments-github-style */ +pre.code.highlight.white, +.code.white { - .hljs { - background: #FFF; - } + background-color: #fff; + color: #333; + pre.highlight, + .line-numbers, .line-numbers a { - color: #999; - } - - .hljs { - display: block; - background: #fff; color: black; - } - - .hljs-comment, - .hljs-template_comment, - .hljs-javadoc, - .hljs-comment * { - color: #006a00; - } - - .hljs-keyword, - .hljs-literal, - .nginx .hljs-title { - color: #aa0d91; - } - .method, - .hljs-list .hljs-title, - .hljs-tag .hljs-title, - .setting .hljs-value, - .hljs-winutils, - .tex .hljs-command, - .http .hljs-title, - .hljs-request, - .hljs-status { - color: #008; - } - - .hljs-envvar, - .tex .hljs-special { - color: #660; - } - - .hljs-string { - color: #c41a16; - } - .hljs-tag .hljs-value, - .hljs-cdata, - .hljs-filter .hljs-argument, - .hljs-attr_selector, - .apache .hljs-cbracket, - .hljs-date, - .hljs-regexp { - color: #080; - } - - .hljs-sub .hljs-identifier, - .hljs-pi, - .hljs-tag, - .hljs-tag .hljs-keyword, - .hljs-decorator, - .ini .hljs-title, - .hljs-shebang, - .hljs-prompt, - .hljs-hexcolor, - .hljs-rules .hljs-value, - .hljs-symbol, - .hljs-symbol .hljs-string, - .hljs-number, - .css .hljs-function, - .clojure .hljs-title, - .clojure .hljs-built_in, - .hljs-function .hljs-title, - .coffeescript .hljs-attribute { - color: #1c00cf; - } - - .hljs-class .hljs-title, - .haskell .hljs-type, - .smalltalk .hljs-class, - .hljs-javadoctag, - .hljs-yardoctag, - .hljs-phpdoc, - .hljs-typename, - .hljs-tag .hljs-attribute, - .hljs-doctype, - .hljs-class .hljs-id, - .hljs-built_in, - .setting, - .hljs-params, - .clojure .hljs-attribute { - color: #5c2699; - } - - .hljs-variable { - color: #3f6e74; - } - .css .hljs-tag, - .hljs-rules .hljs-property, - .hljs-pseudo, - .hljs-subst { - color: #000; - } - - .css .hljs-class, - .css .hljs-id { - color: #9B703F; - } - - .hljs-value .hljs-important { - color: #ff7700; - font-weight: bold; - } - - .hljs-rules .hljs-keyword { - color: #C5AF75; - } - - .hljs-annotation, - .apache .hljs-sqbracket, - .nginx .hljs-built_in { - color: #9B859D; - } - - .hljs-preprocessor, - .hljs-preprocessor *, - .hljs-pragma { - color: #643820; - } - - .tex .hljs-formula { - background-color: #EEE; - font-style: italic; - } - - .diff .hljs-header, - .hljs-chunk { - color: #808080; - font-weight: bold; - } - - .diff .hljs-change { - background-color: #BCCFF9; - } - - .hljs-addition { - background-color: #BAEEBA; - } - - .hljs-deletion { - background-color: #FFC8BD; - } - - .hljs-comment .hljs-yardoctag { - font-weight: bold; - } - - .method .hljs-id { - color: #000; - } -} - -.shadow { - @include box-shadow(0 5px 15px #000); -} - -.file-content { - &.code .white { - .highlight { - border-left: 1px solid #eee; - } - } - - &.wiki .white { - .highlight, pre, .hljs { - background: #F9F9F9; - } - } -} - -.readme-holder .wiki, .note-body, .wiki-holder { - .white { - .highlight, pre, .hljs { - background: #F9F9F9; - } - } + background-color: #fff !important; + color: #333 !important; + } + + pre.code { + border-left: 1px solid #bbb; + } + + // highlight line via anchor + pre .hll { + background-color: #f8eec7 !important; + } + + .hll { background-color: #f8f8f8 } + .c { color: #999988; font-style: italic; } + .err { color: #a61717; background-color: #e3d2d2; } + .k { font-weight: bold; } + .o { font-weight: bold; } + .cm { color: #999988; font-style: italic; } + .cp { color: #999999; font-weight: bold; } + .c1 { color: #999988; font-style: italic; } + .cs { color: #999999; font-weight: bold; font-style: italic; } + .gd { color: #000000; background-color: #ffdddd; } + .gd .x { color: #000000; background-color: #ffaaaa; } + .ge { font-style: italic; } + .gr { color: #aa0000; } + .gh { color: #999999; } + .gi { color: #000000; background-color: #ddffdd; } + .gi .x { color: #000000; background-color: #aaffaa; } + .go { color: #888888; } + .gp { color: #555555; } + .gs { font-weight: bold; } + .gu { color: #800080; font-weight: bold; } + .gt { color: #aa0000; } + .kc { font-weight: bold; } + .kd { font-weight: bold; } + .kn { font-weight: bold; } + .kp { font-weight: bold; } + .kr { font-weight: bold; } + .kt { color: #445588; font-weight: bold; } + .m { color: #009999; } + .s { color: #dd1144; } + .n { color: #333333; } + .na { color: teal; } + .nb { color: #0086b3; } + .nc { color: #445588; font-weight: bold; } + .no { color: teal; } + .ni { color: purple; } + .ne { color: #990000; font-weight: bold; } + .nf { color: #990000; font-weight: bold; } + .nn { color: #555555; } + .nt { color: navy; } + .nv { color: teal; } + .ow { font-weight: bold; } + .w { color: #bbbbbb; } + .mf { color: #009999; } + .mh { color: #009999; } + .mi { color: #009999; } + .mo { color: #009999; } + .sb { color: #dd1144; } + .sc { color: #dd1144; } + .sd { color: #dd1144; } + .s2 { color: #dd1144; } + .se { color: #dd1144; } + .sh { color: #dd1144; } + .si { color: #dd1144; } + .sx { color: #dd1144; } + .sr { color: #009926; } + .s1 { color: #dd1144; } + .ss { color: #990073; } + .bp { color: #999999; } + .vc { color: teal; } + .vg { color: teal; } + .vi { color: teal; } + .il { color: #009999; } + .gc { color: #999; background-color: #EAF2F5; } } diff --git a/app/assets/stylesheets/main/fonts.scss b/app/assets/stylesheets/main/fonts.scss deleted file mode 100644 index f945aaca848..00000000000 --- a/app/assets/stylesheets/main/fonts.scss +++ /dev/null @@ -1,3 +0,0 @@ -/** Typo **/ -$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; -$regular_font: "Helvetica Neue", Helvetica, Arial, sans-serif; diff --git a/app/assets/stylesheets/main/variables.scss b/app/assets/stylesheets/main/variables.scss deleted file mode 100644 index c71984a5665..00000000000 --- a/app/assets/stylesheets/main/variables.scss +++ /dev/null @@ -1,49 +0,0 @@ -/* - * General Colors - */ -$style_color: #474D57; -$hover: #FFECDB; -$box_bg: #F9F9F9; - -/* - * Link colors - */ -$link_color: #446e9b; -$link_hover_color: darken($link-color, 10%); - -$btn-border: 1px solid #ccc; - -/* - * Success colors (green) - */ -$border_success: #019875; -$bg_success: #019875; - -/* - * Danger colors (red) - */ -$border_danger: #d43f3a; -$bg_danger: #d9534f; - -/* - * Primary colors (blue) - */ -$border_primary: #446e9b; -$bg_primary: #446e9b; - -/* - * Warning colors (yellow) - */ -$bg_warning: #EB9532; -$border_warning: #EB9532; - -/** - * Commit Diff Colors - */ -$added: #63c363; -$deleted: #f77; - -/** - * - */ -$nprogress-color: #3498db; diff --git a/app/assets/stylesheets/sections/admin.scss b/app/assets/stylesheets/pages/admin.scss index a51deee7970..144852e7874 100644 --- a/app/assets/stylesheets/sections/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -50,3 +50,14 @@ line-height: 2; } } + +.broadcast-message { + @extend .alert-warning; + padding: 10px; + text-align: center; +} + +.broadcast-message-preview { + @extend .broadcast-message; + margin-bottom: 20px; +} diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss new file mode 100644 index 00000000000..e7125c03993 --- /dev/null +++ b/app/assets/stylesheets/pages/commit.scss @@ -0,0 +1,123 @@ +.commit-title{ + display: block; +} + +.commit-title{ + margin-bottom: 10px; +} + +.commit-author, .commit-committer{ + display: block; + color: #999; + font-weight: normal; + font-style: italic; +} + +.commit-author strong, .commit-committer strong{ + font-weight: bold; + font-style: normal; +} + +.commit-description { + background: none; + border: none; + margin: 0; + padding: 0; + margin-top: 10px; +} + +.commit-stat-summary { + color: #666; + font-size: 14px; + font-weight: normal; + padding: 3px 0; + margin-bottom: 10px; +} + +.commit-info-row { + margin-bottom: 10px; + .avatar { + @extend .avatar-inline; + } + .commit-committer-link, + .commit-author-link { + color: #444; + font-weight: bold; + } +} + +.commit-box { + margin: 10px 0; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + padding: 20px 0; + + .commit-title { + margin: 0; + } + + .commit-description { + margin-top: 15px; + } +} + +.file-stats a { + color: $style_color; +} + +.file-stats { + .new-file { + a { + color: #090; + } + i { + color: #1BCF00; + } + } + .renamed-file { + i { + color: #FE9300; + } + } + .deleted-file { + a { + color: #B00; + } + i { + color: #EE0000; + } + } + .edit-file{ + i{ + color: #555; + } + } +} + +/* + * Commit message textarea for web editor and + * custom merge request message + */ +.commit-message-container { + background-color: $body-bg; + position: relative; + font-family: $monospace_font; + $left: 12px; + .max-width-marker { + width: 72ch; + color: rgba(0, 0, 0, 0.0); + font-family: inherit; + left: $left; + height: 100%; + border-right: 1px solid mix($input-border, white); + position: absolute; + z-index: 1; + } + > textarea { + background-color: rgba(0, 0, 0, 0.0); + font-family: inherit; + padding-left: $left; + position: relative; + z-index: 2; + } +} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss new file mode 100644 index 00000000000..359f4073e87 --- /dev/null +++ b/app/assets/stylesheets/pages/commits.scss @@ -0,0 +1,114 @@ +.commits-compare-switch{ + @extend .btn; + background: image-url("switch_icon.png") no-repeat center center; + text-indent: -9999px; + float: left; + margin-right: 9px; +} + +.lists-separator { + margin: 10px 0; + border-color: #DDD; +} + +.commits-row { + ul { + margin: 0; + + li.commit { + padding: 8px 0; + } + } + + .commits-row-date { + font-size: 15px; + line-height: 20px; + margin-bottom: 5px; + } +} + +.commits-feed-holder { + float: right; +} + +li.commit { + .commit-row-title { + font-size: $list-font-size; + line-height: 20px; + margin-bottom: 2px; + + .notes_count { + float: right; + margin-right: 10px; + } + + .commit_short_id { + min-width: 65px; + font-family: $monospace_font; + } + + .str-truncated { + max-width: 70%; + } + + .commit-row-message { + color: #444; + + &:hover { + text-decoration: underline; + } + } + + .text-expander { + background: #eee; + color: #555; + padding: 0 5px; + cursor: pointer; + margin-left: 4px; + &:hover { + background-color: #ddd; + } + } + } + + .commit-row-description { + font-size: 14px; + border-left: 1px solid #EEE; + padding: 10px 15px; + margin: 5px 0 10px 5px; + background: #f9f9f9; + display: none; + + pre { + border: none; + background: inherit; + padding: 0; + margin: 0; + } + } + + .commit-row-info { + color: #777; + line-height: 24px; + font-size: 13px; + + a { + color: #777; + } + + .committed_ago { + display: inline-block; + } + } + + &.inline-commit { + .commit-row-title { + font-size: 13px; + } + + .committed_ago { + float: right; + @extend .cgray; + } + } +} diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index d181d83e857..9a3b543ad10 100644 --- a/app/assets/stylesheets/sections/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -23,42 +23,6 @@ } } -.dashboard { - .dash-filter { - width: 205px; - float: left; - height: inherit; - } -} - -@media (max-width: 1200px) { - .dashboard .dash-filter { - width: 140px; - } -} - -.dash-sidebar-tabs { - margin-bottom: 2px; - border: none; - margin: 0 !important; - - li { - &.active { - a { - background-color: #EEE; - border-bottom: 1px solid #EEE !important; - &:hover { - background: #eee; - } - } - } - - a { - border-color: #DDD !important; - } - } -} - .project-row, .group-row { padding: 0 !important; font-size: 14px; @@ -98,7 +62,6 @@ margin-left: 10px; float: left; margin-right: 15px; - font-size: 20px; margin-bottom: 15px; i { @@ -106,8 +69,21 @@ } } +.dash-project-avatar { + float: left; + + .avatar { + margin-top: -8px; + margin-left: -15px; + @include border-radius(0px); + } + .identicon { + line-height: 40px; + } +} + .dash-project-access-icon { float: left; - margin-right: 3px; + margin-right: 5px; width: 16px; } diff --git a/app/assets/stylesheets/sections/diff.scss b/app/assets/stylesheets/pages/diff.scss index 758f15c8013..af6ea58382f 100644 --- a/app/assets/stylesheets/sections/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1,24 +1,37 @@ .diff-file { - border: 1px solid #CCC; + border: 1px solid $border-color; margin-bottom: 1em; .diff-header { - @extend .clearfix; - background: #EEE; - border-bottom: 1px solid #CCC; - padding: 5px 5px 5px 10px; + position: relative; + background: $background-color; + border-bottom: 1px solid $border-color; + padding: 10px 15px; color: #555; + z-index: 10; > span { font-family: $monospace_font; - line-height: 2; + word-break: break-all; + margin-right: 200px; + display: block; + + .file-mode { + margin-left: 10px; + color: #777; + } } .diff-btn-group { float: right; + position: absolute; + top: 5px; + right: 15px; .btn { - background-color: #FFF; + padding: 0px 10px; + font-size: 13px; + line-height: 28px; } } @@ -26,26 +39,21 @@ font-family: $monospace_font; font-size: smaller; } - - .file-mode { - font-family: $monospace_font; - margin-left: 10px; - } } .diff-content { overflow: auto; overflow-y: hidden; background: #FFF; color: #333; - font-size: 12px; + font-size: $code_font_size; .old { span.idiff { - background-color: #F99; + background-color: #f8cbcb; } } .new { span.idiff { - background-color: #8F8; + background-color: #a6f3a6; } } .unfold { @@ -64,8 +72,8 @@ margin: 0px; padding: 0px; td { - line-height: 18px; - font-size: 12px; + line-height: $code_line_height; + font-size: $code_font_size; } } @@ -83,10 +91,10 @@ margin: 0px; padding: 0px; border: none; - background: #F5F5F5; - color: #666; + background: $background-color; + color: rgba(0,0,0,0.3); padding: 0px 5px; - border-right: 1px solid #ccc; + border-right: 1px solid $border-color; text-align: right; min-width: 35px; max-width: 50px; @@ -96,7 +104,7 @@ float: left; width: 35px; font-weight: normal; - color: #666; + color: rgba(0,0,0,0.3); &:hover { text-decoration: underline; } @@ -114,13 +122,13 @@ .line_holder { &.old .old_line, &.old .new_line { - background: #FCC; - border-color: #E7BABA; + background: #ffdddd; + border-color: #f1c0c0; } &.new .old_line, &.new .new_line { - background: #CFC; - border-color: #B9ECB9; + background: #dbffdb; + border-color: #c1e9c1; } } .line_content { @@ -129,13 +137,13 @@ padding: 0px 0.5em; border: none; &.new { - background: #CFD; + background: #eaffea; } &.old { - background: #FDD; + background: #ffecec; } &.matched { - color: #ccc; + color: $border-color; background: #fafafa; } &.parallel { diff --git a/app/assets/stylesheets/sections/editor.scss b/app/assets/stylesheets/pages/editor.scss index f62f46ee168..759ba6b1c22 100644 --- a/app/assets/stylesheets/sections/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -16,8 +16,6 @@ } } .commit-button-annotation { - @extend .alert; - @extend .alert-info; display: inline-block; margin: 0; padding: 2px; @@ -31,4 +29,26 @@ margin: 5px 8px 0 8px; } } + + .file-title { + @extend .monospace; + font-size: 14px; + padding: 5px; + } + + .editor-ref { + background: $background-color; + padding: 11px 15px; + border-right: 1px solid #CCC; + display: inline-block; + margin: -5px -5px; + margin-right: 10px; + } + + .editor-file-name { + .new-file-name { + display: inline-block; + width: 200px; + } + } } diff --git a/app/assets/stylesheets/sections/errors.scss b/app/assets/stylesheets/pages/errors.scss index 32d2d7b1dbf..32d2d7b1dbf 100644 --- a/app/assets/stylesheets/sections/errors.scss +++ b/app/assets/stylesheets/pages/errors.scss diff --git a/app/assets/stylesheets/sections/events.scss b/app/assets/stylesheets/pages/events.scss index 485a9c46610..d4af7506d5b 100644 --- a/app/assets/stylesheets/sections/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -45,35 +45,39 @@ padding: 12px 0px; border-bottom: 1px solid #eee; .event-title { - @include str-truncated(72%); - color: #333; - font-weight: normal; + max-width: 70%; + @include str-truncated(calc(100% - 174px)); + font-weight: 500; font-size: 14px; .author_name { color: #333; } } .event-body { + font-size: 13px; margin-left: 35px; - margin-right: 100px; + margin-right: 80px; + color: #777; - .event-info { - color: #666; - } .event-note { - color: #666; margin-top: 5px; + word-wrap: break-word; .md { font-size: 13px; + + iframe.twitter-share-button { + vertical-align: bottom; + } } pre { border: none; background: #f9f9f9; border-radius: 0; - color: #666; + color: #777; margin: 0 20px; + overflow: hidden; } .note-image-attach { @@ -120,7 +124,6 @@ padding: 3px; padding-left: 0; border: none; - color: #666; .commit-row-title { font-size: 12px; } @@ -144,44 +147,20 @@ } } -/** - * Event filter - * - */ -.event_filter { - position: absolute; - width: 40px; - margin-left: -55px; - - .filter_icon { - a { - text-align:center; - background: $bg_primary; - margin-bottom: 10px; - float: left; - padding: 9px 6px; - font-size: 18px; - width: 40px; - color: #FFF; - @include border-radius(3px); - } - - &.inactive { - a { - color: #DDD; - background: #f9f9f9; - } - } - } -} /* * Last push widget */ .event-last-push { + overflow: auto; .event-last-push-text { - @include str-truncated(75%); - line-height: 24px; + @include str-truncated(100%); + padding: 5px 0; + font-size: 13px; + float:left; + margin-right: -150px; + padding-right: 150px; + line-height: 20px; } } @@ -207,3 +186,12 @@ } } } + +.event_filter { + li a { + font-size: 13px; + padding: 5px 10px; + background: $background-color; + margin-left: 4px; + } +} diff --git a/app/assets/stylesheets/sections/explore.scss b/app/assets/stylesheets/pages/explore.scss index 9b92128624c..9b92128624c 100644 --- a/app/assets/stylesheets/sections/explore.scss +++ b/app/assets/stylesheets/pages/explore.scss diff --git a/app/assets/stylesheets/sections/graph.scss b/app/assets/stylesheets/pages/graph.scss index 3d878d1e528..c3b10d144e1 100644 --- a/app/assets/stylesheets/sections/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -1,11 +1,11 @@ .project-network { - border: 1px solid #CCC; + border: 1px solid $border-color; .controls { color: #888; font-size: 14px; padding: 5px; - border-bottom: 1px solid #bbb; + border-bottom: 1px solid $border-color; background: #EEE; } diff --git a/app/assets/stylesheets/sections/groups.scss b/app/assets/stylesheets/pages/groups.scss index e49fe1a9dd6..2b1b747139a 100644 --- a/app/assets/stylesheets/sections/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -1,6 +1,5 @@ .new-group-member-holder { margin-top: 50px; - background: #f9f9f9; padding-top: 20px; } diff --git a/app/assets/stylesheets/sections/help.scss b/app/assets/stylesheets/pages/help.scss index 07c62f98c36..6da7a2511a2 100644 --- a/app/assets/stylesheets/sections/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -12,7 +12,6 @@ color: #888; a { - font-size: 14px; margin-right: 3px; } } @@ -29,7 +28,6 @@ th { padding-top: 15px; - font-size: 14px; line-height: 1.5; color: #333; text-align: left diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss new file mode 100644 index 00000000000..3df4bb84bd2 --- /dev/null +++ b/app/assets/stylesheets/pages/import.scss @@ -0,0 +1,18 @@ +i.icon-gitorious { + display: inline-block; + background-position: 0px 0px; + background-size: contain; + background-repeat: no-repeat; +} + +i.icon-gitorious-small { + background-image: image-url('gitorious-logo-blue.png'); + width: 13px; + height: 13px; +} + +i.icon-gitorious-big { + background-image: image-url('gitorious-logo-black.png'); + width: 18px; + height: 18px; +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss new file mode 100644 index 00000000000..586e7b5f8da --- /dev/null +++ b/app/assets/stylesheets/pages/issuable.scss @@ -0,0 +1,47 @@ +@media (max-width: $screen-sm-max) { + .issuable-affix { + margin-top: 20px; + } +} + +@media (max-width: $screen-md-max) { + .issuable-affix { + position: static; + } +} + +@media (min-width: $screen-md-max) { + .issuable-affix { + &.affix-top { + position: static; + } + + &.affix { + position: fixed; + top: 70px; + margin-right: 35px; + } + } +} + +.issuable-context-title { + font-size: 14px; + line-height: 1.4; + margin-bottom: 5px; + + .avatar { + margin-left: 0; + } + + label { + color: #666; + font-weight: normal; + margin-right: 4px; + } +} + +.issuable-affix .context { + font-size: 13px; + + .btn { font-size: 13px; } +} diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/pages/issues.scss index 9a5400fffbc..ed938f86b35 100644 --- a/app/assets/stylesheets/sections/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -5,11 +5,13 @@ .issue-title { margin-bottom: 5px; - font-size: 14px; + font-size: $list-font-size; + font-weight: bold; } .issue-info { color: #999; + font-size: 13px; } .issue-check { @@ -23,28 +25,16 @@ display: inline-block; } - .issue-actions { - display: none; - position: absolute; - top: 10px; - right: 15px; - } - - &:hover { - .issue-actions { - display: block; - } + .issue-no-comments { + opacity: 0.5; } } } .check-all-holder { - height: 32px; + line-height: 36px; float: left; - margin-right: 12px; - padding: 6px 15px; - border: 1px solid #ccc; - @include border-radius(4px); + margin-right: 15px; } .issues_content { @@ -57,33 +47,10 @@ } } -@media (min-width: 800px) { .issues_filters select { width: 160px; } } -@media (min-width: 1200px) { .issues_filters select { width: 220px; } } - -@media (min-width: 800px) { .issues_bulk_update .select2-container { min-width: 120px; } } -@media (min-width: 1200px) { .issues_bulk_update .select2-container { min-width: 160px; } } - -.issues_bulk_update { - .select2-container .select2-choice { - color: #444 !important; - font-weight: 500; - } -} - -#update_status { - width: 100px; -} - .participants { margin-bottom: 20px; } -.issues_bulk_update { - .select2-container { - text-shadow: none; - } -} - .issue-search-form { margin: 0; height: 24px; @@ -94,8 +61,15 @@ } } -.issue-show-labels .color-label { - padding: 6px 10px; +.issue-show-labels { + a { + margin-right: 5px; + margin-bottom: 5px; + display: inline-block; + .color-label { + padding: 6px 10px; + } + } } form.edit-issue { @@ -110,12 +84,12 @@ form.edit-issue { } &.closed { - background: #F5f5f5; + background: #F9F9F9; border-color: #E5E5E5; } &.merged { - background: #F5f5f5; + background: #F9F9F9; border-color: #E5E5E5; } } @@ -162,3 +136,18 @@ form.edit-issue { } } } + +h2.issue-title { + margin-top: 0; + font-weight: bold; +} + +.issue-form .select2-container { + width: 250px !important; +} + +.issues-holder { + .issue-info { + margin-left: 20px; + } +} diff --git a/app/assets/stylesheets/sections/labels.scss b/app/assets/stylesheets/pages/labels.scss index d1590e42fcb..d1590e42fcb 100644 --- a/app/assets/stylesheets/sections/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss new file mode 100644 index 00000000000..83b866c3a64 --- /dev/null +++ b/app/assets/stylesheets/pages/login.scss @@ -0,0 +1,124 @@ +/* Login Page */ +.login-page { + .container { + max-width: 960px; + } + + .navbar-gitlab .container { + max-width: none; + } + + .brand-holder { + font-size: 18px; + line-height: 1.5; + + p { + color: #888; + } + + h1:first-child { + font-weight: normal; + margin-bottom: 30px; + } + + img { + max-width: 100%; + margin-bottom: 30px; + } + + a { + font-weight: bold; + } + } + + .login-box{ + background: #fafafa; + border-radius: 10px; + box-shadow: 0 0px 2px #CCC; + padding: 15px; + + .login-heading h3 { + font-weight: 300; + line-height: 1.5; + margin: 0 0 10px 0; + } + + .login-footer { + margin-top: 10px; + + p:last-child { + margin-bottom: 0; + } + } + + a.forgot { + float: right; + padding-top: 6px + } + + .nav .active a { + background: transparent; + } + } + + .form-control { + font-size: 14px; + padding: 10px 8px; + width: 100%; + height: auto; + + &.top { + @include border-radius(5px 5px 0 0); + margin-bottom: 0px; + } + + &.bottom { + @include border-radius(0 0 5px 5px); + border-top: 0; + margin-bottom: 20px; + } + + &.middle { + border-top: 0; + margin-bottom:0px; + @include border-radius(0); + } + + &:active, &:focus { + background-color: #FFF; + } + } + + .devise-errors { + h2 { + margin-top: 0; + font-size: 14px; + color: #a00; + } + } + + .remember-me { + margin-top: -10px; + + label { + font-weight: normal; + } + } +} + +@media (max-width: $screen-xs-max) { + .login-page { + .col-sm-5.pull-right { + float: none !important; + } + } +} + +.oauth-image-link { + margin-right: 10px; + + img { + width: 32px; + height: 32px; + } +} diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index ec844cc00b0..61071320973 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -3,7 +3,7 @@ * MR -> show: Automerge widget * */ -.automerge_widget { +.mr-state-widget { form { margin-bottom: 0; .clearfix { @@ -11,25 +11,36 @@ } } - .accept-group { - label { - margin: 5px; + .accept-merge-holder { + .accept-action { + display: inline-block; + } + + .accept-control { + display: inline-block; + margin: 0; margin-left: 20px; + padding: 10px 0; + line-height: 20px; + font-weight: bold; + + .remove_source_checkbox { + margin: 0; + font-weight: bold; + } } } } -.merge-request .merge-request-tabs{ - border-bottom: 2px solid $border_primary; - margin: 20px 0; - - li { - a { - padding: 15px 40px; - font-size: 14px; - margin-bottom: -2px; - border-bottom: 2px solid $border_primary; - @include border-radius(0px); +@media(min-width: $screen-sm-max) { + .merge-request .merge-request-tabs{ + margin: 20px 0; + + li { + a { + padding: 15px 40px; + font-size: 14px; + } } } } @@ -73,16 +84,23 @@ .merge-request-title { margin-bottom: 5px; - font-size: 14px; + font-size: $list-font-size; + font-weight: bold; } .merge-request-info { color: #999; - - .merge-request-labels { - display: inline-block; - } + font-size: 13px; } + + } + + .merge-request-labels { + display: inline-block; + } + + .merge-request-no-comments { + opacity: 0.5; } } @@ -104,45 +122,32 @@ } .mr-state-widget { - background: $box_bg; + font-size: 13px; + background: #FAFAFA; margin-bottom: 20px; - @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); + color: #666; + border: 1px solid #e5e5e5; + @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.05)); + @include border-radius(3px); .ci_widget { padding: 10px 15px; font-size: 15px; - border-bottom: 1px solid #BBB; - color: #777; - background-color: #F5F5F5; + border-bottom: 1px solid #EEE; &.ci-success { - color: $bg_success; - border-color: $border_success; - background-color: #F1FAF1; - } - - &.ci-pending { - color: #548; - border-color: #548; - background-color: #F4F1FA; + color: $gl-success; } + &.ci-pending, &.ci-running { - color: $bg_warning; - border-color: $border_warning; - background-color: #FAF5F1; - } - - &.ci-failed { - color: $bg_danger; - border-color: $border_danger; - background-color: #FAF1F1; + color: $gl-warning; } + &.ci-failed, + &.ci-canceled, &.ci-error { - color: $bg_danger; - border-color: $border_danger; - background-color: #FAF1F1; + color: $gl-danger; } } @@ -150,8 +155,8 @@ padding: 10px 15px; h4 { - font-size: 20px; - font-weight: normal; + font-weight: bold; + margin: 5px 0; } p:last-child { @@ -169,10 +174,17 @@ } } -.merge-request-show-labels .label { - padding: 6px 10px; +.merge-request-show-labels { + a { + margin-right: 5px; + margin-bottom: 5px; + display: inline-block; + .color-label { + padding: 6px 10px; + } + } } -.mr-commits .commit { - padding: 10px 15px; +.merge-request-form .select2-container { + width: 250px !important; } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss new file mode 100644 index 00000000000..15e3948e402 --- /dev/null +++ b/app/assets/stylesheets/pages/milestone.scss @@ -0,0 +1,9 @@ +.issues-sortable-list .str-truncated { + max-width: 90%; +} + +li.milestone { + h4 { + font-weight: bold; + } +} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss new file mode 100644 index 00000000000..203f9374cee --- /dev/null +++ b/app/assets/stylesheets/pages/note_form.scss @@ -0,0 +1,171 @@ +/** + * Note Form + */ + +.comment-btn { + @extend .btn-create; +} +.reply-btn { + @extend .btn-primary; +} +.diff-file .diff-content { + tr.line_holder:hover { + &> td.line_content { + background: $hover !important; + border-color: darken($hover, 10%) !important; + } + &> td.new_line, + &> td.old_line { + background: darken($hover, 4%) !important; + border-color: darken($hover, 10%) !important; + } + } + + tr.line_holder:hover > td .line_note_link { + opacity: 1.0; + filter: alpha(opacity=100); + } +} +.diff-file, +.discussion { + .new_note { + margin: 0; + border: none; + } +} +.new_note { + display: none; +} + +.new_note, .edit_note { + .buttons { + margin-top: 8px; + margin-bottom: 3px; + } + + .note-preview-holder { + > p { + overflow-x: auto; + } + } + + img { + max-width: 100%; + } + + .note_text { + width: 100%; + } +} + +/* loading indicator */ +.notes-busy { + margin: 18px; +} + +.note-image-attach { + @extend .col-md-4; + @extend .thumbnail; + margin-left: 45px; + float: none; +} + +.common-note-form { + margin: 0; + background: #F9F9F9; + padding: 5px; + border: 1px solid #DDD; +} + +.note-form-actions { + background: #F9F9F9; + + .note-form-option { + margin-top: 8px; + margin-left: 30px; + @extend .pull-left; + } + + .js-notify-commit-author { + float: left; + } + + .write-preview-btn { + // makes the "absolute" position for links relative to this + position: relative; + + // preview/edit buttons + > a { + position: absolute; + right: 5px; + top: 8px; + } + } +} + +.note-edit-form { + display: none; + font-size: 13px; + + .form-actions { + padding-left: 20px; + + .btn-save { + float: left; + } + + .note-form-option { + float: left; + padding: 2px 0 0 25px; + } + } +} + +.js-note-attachment-delete { + display: none; +} + +.parallel-comment { + padding: 6px; +} + +.error-alert > .alert { + margin-top: 5px; + margin-bottom: 5px; +} + +.discussion-body, +.diff-file { + .notes .note { + border-color: #ddd; + padding: 10px 15px; + } + + .discussion-reply-holder { + background: #f9f9f9; + padding: 10px 15px; + border-top: 1px solid #DDD; + } +} + +.discussion-notes-count { + font-size: 16px; +} + +.edit_note { + .markdown-area { + min-height: 140px; + } + .note-form-actions { + background: transparent; + } +} + +.comment-hints { + color: #999; + background: #FFF; + padding: 5px; + margin-top: -11px; + border: 1px solid #DDD; + font-size: 13px; +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss new file mode 100644 index 00000000000..42b8ecabb38 --- /dev/null +++ b/app/assets/stylesheets/pages/notes.scss @@ -0,0 +1,228 @@ +/** + * Notes + */ + +@-webkit-keyframes targe3-note { + from { background:#fffff0; } + 50% { background:#ffffd3; } + to { background:#fffff0; } +} + +ul.notes { + display: block; + list-style: none; + margin: 0px; + padding: 0px; + + .discussion-header, + .note-header { + @extend .cgray; + padding-bottom: 15px; + + a:hover { + text-decoration: none; + } + + .avatar { + float: left; + margin-right: 10px; + } + + .discussion-last-update, + .note-last-update { + &:before { + content: "\00b7"; + } + + font-size: 13px; + + a { + @extend .cgray; + + &:hover { + text-decoration: underline; + } + } + } + .author { + color: #333; + font-weight: bold; + &:hover { + color: $gl-link-color; + } + } + .author-username { + } + + .note-role { + float: right; + margin-top: 1px; + border: 1px solid #bbb; + background-color: transparent; + color: #999; + } + } + + .discussion { + overflow: hidden; + display: block; + position:relative; + } + + .note { + display: block; + position:relative; + .note-body { + overflow: auto; + .note-text { + overflow: auto; + word-wrap: break-word; + @include md-typography; + + // Reduce left padding of first task list ul element + ul.task-list:first-child { + padding-left: 10px; + + // sub-tasks should be padded normally + ul { + padding-left: 20px; + } + } + + hr { + margin: 10px 0; + } + } + } + .note-header { + padding-bottom: 3px; + } + + &:last-child { + border-bottom: none; + } + } +} + +// Diff code in discussion view +.discussion-body .diff-file { + .diff-header > span { + margin-right: 10px; + } + .line_content { + white-space: pre-wrap; + } +} + +.diff-file .notes_holder { + font-size: 13px; + line-height: 18px; + font-family: $regular_font; + + td { + border: 1px solid #ddd; + border-left: none; + + &.notes_line { + text-align: center; + padding: 10px 0; + background: #FFF; + } + &.notes_line2 { + text-align: center; + padding: 10px 0; + border-left: 1px solid #ddd !important; + } + &.notes_content { + background-color: #fff; + border-width: 1px 0; + padding-top: 0; + vertical-align: top; + &.parallel{ + border-width: 1px; + } + } + } +} + +/** + * Actions for Discussions/Notes + */ + +.discussion, +.note { + .discussion-actions, + .note-actions { + float: right; + margin-left: 10px; + + a { + margin-left: 5px; + + color: #999; + + i.fa { + font-size: 16px; + line-height: 16px; + } + + &:hover { + @extend .cgray; + &.danger { @extend .cred; } + } + } + } +} +.diff-file .note .note-actions { + right: 0; + top: 0; +} + + +/** + * Line note button on the side of diffs + */ + +.diff-file tr.line_holder { + @mixin show-add-diff-note { + filter: alpha(opacity=100); + opacity: 1.0; + } + + .add-diff-note { + margin-top: -4px; + @include border-radius(40px); + background: #FFF; + padding: 4px; + font-size: 16px; + color: $gl-link-color; + margin-left: -60px; + position: absolute; + z-index: 10; + width: 32px; + + transition: all 0.2s ease; + + // "hide" it by default + opacity: 0.0; + filter: alpha(opacity=0); + + &:hover { + width: 38px; + font-size: 20px; + background: $gl-info; + color: #FFF; + @include show-add-diff-note; + } + } + + // "show" the icon also if we just hover somewhere over the line + &:hover > td { + background: $hover !important; + + .add-diff-note { + @include show-add-diff-note; + } + } +} + diff --git a/app/assets/stylesheets/sections/notifications.scss b/app/assets/stylesheets/pages/notifications.scss index f11c5dff4ab..cc273f55222 100644 --- a/app/assets/stylesheets/sections/notifications.scss +++ b/app/assets/stylesheets/pages/notifications.scss @@ -10,13 +10,13 @@ } .ns-part { - color: $bg_primary; + color: $gl-primary; } .ns-watch { - color: $bg_success; + color: $gl-success; } .ns-mute { - color: $bg_danger; + color: $gl-danger; } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss new file mode 100644 index 00000000000..8e4f0eb2b25 --- /dev/null +++ b/app/assets/stylesheets/pages/profile.scss @@ -0,0 +1,49 @@ +.account-page { + fieldset { + margin-bottom: 15px; + padding-bottom: 15px; + } +} + +.btn-build-token { + float: left; + padding: 6px 20px; + margin-right: 12px; +} + +.profile-avatar-form-option { + hr { + margin: 10px 0; + } +} + +.oauth-buttons { + .btn-group { + margin-right: 10px; + } + + .btn { + line-height: 40px; + height: 42px; + padding: 0px 12px; + + img { + width: 32px; + height: 32px; + } + } +} + +// Profile > Account > Two Factor Authentication +.two-factor-new { + .manual-instructions { + h3 { + margin-top: 0; + } + + // Slightly increase the size of the details so they're easier to read + dl { + font-size: 1.1em; + } + } +} diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss new file mode 100644 index 00000000000..e5859fe7384 --- /dev/null +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -0,0 +1,56 @@ +.application-theme { + label { + margin-right: 20px; + text-align: center; + + .preview { + @include border-radius(4px); + + height: 80px; + margin-bottom: 10px; + width: 160px; + + &.ui_blue { + background: $theme-blue; + } + + &.ui_charcoal { + background: $theme-charcoal; + } + + &.ui_graphite { + background: $theme-graphite; + } + + &.ui_gray { + background: $theme-gray; + } + + &.ui_green { + background: $theme-green; + } + + &.ui_violet { + background: $theme-violet; + } + } + } +} + +.syntax-theme { + label { + margin-right: 20px; + text-align: center; + + .preview { + margin-bottom: 10px; + width: 160px; + + img { + @include border-radius(4px); + + max-width: 100%; + } + } + } +} diff --git a/app/assets/stylesheets/sections/projects.scss b/app/assets/stylesheets/pages/projects.scss index 7b894cf00bb..e19b2eafa43 100644 --- a/app/assets/stylesheets/sections/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -15,65 +15,76 @@ } .project-home-panel { + margin-top: 10px; margin-bottom: 15px; + position: relative; + padding-left: 65px; + min-height: 50px; + + .project-identicon-holder { + position: absolute; + left: 0; + top: -14px; + + .avatar { + width: 50px; + height: 50px; + } - &.empty-project { - border-bottom: 0px; - padding-bottom: 15px; - margin-bottom: 0px; - } - - .project-home-dropdown { - margin-left: 10px; - float: right; + .identicon { + font-size: 26px; + line-height: 50px; + } } .project-home-row { @extend .clearfix; margin-bottom: 15px; + &.project-home-row-top { + margin-bottom: 15px; + } + .project-home-desc { + color: $gray; float: left; - color: #666; font-size: 16px; - } + line-height: 1.3; + margin-right: 250px; - .star-fork-buttons { - float: right; - min-width: 200px; - font-size: 14px; - font-weight: bold; - - .star-buttons, .fork-buttons { - float: right; - margin-left: 20px; - - a:hover { - text-decoration: none; - } - - .count { - margin-left: 5px; - } + // Render Markdown-generated HTML inline for this block + p { + display: inline; } } } .visibility-level-label { - color: #555; - font-weight: bold; + color: $gray; i { color: inherit; } } -} -.project-home-links { - padding: 10px 0px; - float: right; - a { - margin-left: 10px; - font-weight: 500; + .project-repo-buttons { + margin-top: -3px; + position: absolute; + right: 0; + width: 265px; + text-align: right; + + .btn { + font-weight: bold; + font-size: 14px; + line-height: 16px; + + .count { + padding-left: 10px; + border-left: 1px solid #ccc; + display: inline-block; + margin-left: 10px; + } + } } } @@ -82,29 +93,25 @@ margin-right: 45px; } - .btn, - .form-control { - border: 1px solid #E1E1E1; - box-shadow: none; - padding: 6px 9px; - } - - .btn { - background: none; - color: $link_color; - - &.active { - color: #333; - font-weight: bold; - } - } - .form-control { cursor: auto; @extend .monospace; background: #FAFAFA; width: 100%; } + + .input-group-addon { + background: #FAFAFA; + + &.git-protocols { + padding: 0; + border: none; + + .input-group-btn:last-child > .btn { + @include border-radius-right(0); + } + } + } } .project-visibility-level-holder { @@ -122,8 +129,8 @@ } .option-descr { - margin-left: 24px; - color: #666; + margin-left: 36px; + color: $gray; } } } @@ -156,7 +163,7 @@ ul.nav.nav-projects-tabs { } } -.team_member_row form { +.project_member_row form { margin: 0px; } @@ -195,47 +202,40 @@ ul.nav.nav-projects-tabs { } .project-side { - .btn-block { - background-image: none; - - .btn, &.btn { - white-space: normal; - text-align: left; - padding: 10px 15px; - background-color: #F9F9F9; - border-color: #DDD; + .project-fork-icon { + float: left; + font-size: 26px; + margin-right: 10px; + line-height: 1.5; + } - &:hover { - background-color: #eee; - border-color: #DDD; - } + .panel { + @include border-radius(3px); + + .panel-heading, .panel-footer { + font-weight: normal; + background-color: transparent; + color: #666; + border-color: #EEE; } - .count { - float: right; - font-weight: 500; - text-shadow: 0 1px #FFF; + .actions { + margin-top: 10px; } - &.btn-group-justified { - .btn { - width: 100%; - } - .dropdown-toggle { - width: 30px; - padding: 10px; - } - ul { - width: 100%; - } + .nav-pills a { + padding: 10px; + font-weight: bold; + color: $gl-link-color; + } + + .nav { + margin-bottom: 15px; } } - .project-fork-icon { - float: left; - font-size: 26px; - margin-right: 10px; - line-height: 1.5; + .ci-status-image { + max-height: 22px; } } @@ -248,19 +248,20 @@ ul.nav.nav-projects-tabs { } .vs-public { - color: $bg_primary; + color: $gl-primary; } .vs-internal { - color: $bg_warning; + color: $gl-warning; } .vs-private { - color: $bg_success; + color: $gl-success; } .breadcrumb.repo-breadcrumb { - padding: 2px 0; + padding: 0; + line-height: 34px; background: white; border: none; font-size: 16px; @@ -296,15 +297,13 @@ ul.nav.nav-projects-tabs { } } -@media (max-width: $screen-xs-max) { - .project-home-panel { - .star-fork-buttons { - padding-top: 10px; - padding-right: 15px; - } +table.table.protected-branches-list tr.no-border { + th, td { + border: 0; } +} - .project-home-links { - display: none; - } +.project-import .btn { + float: left; + margin-right: 10px; } diff --git a/app/assets/stylesheets/sections/search.scss b/app/assets/stylesheets/pages/search.scss index bdaa17ac339..bdaa17ac339 100644 --- a/app/assets/stylesheets/sections/search.scss +++ b/app/assets/stylesheets/pages/search.scss diff --git a/app/assets/stylesheets/sections/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index d79591d9915..d79591d9915 100644 --- a/app/assets/stylesheets/sections/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss diff --git a/app/assets/stylesheets/sections/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index b9be47e7700..b9be47e7700 100644 --- a/app/assets/stylesheets/sections/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss diff --git a/app/assets/stylesheets/sections/tree.scss b/app/assets/stylesheets/pages/tree.scss index 678a6cd716d..34ee4d7b31e 100644 --- a/app/assets/stylesheets/sections/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -17,19 +17,6 @@ @include border-radius(0); tr { - td, th { - padding: 8px 10px; - line-height: 20px; - } - th { - font-weight: normal; - font-size: 15px; - border-bottom: 1px solid #CCC !important; - } - td { - border-color: #F1F1F1 !important; - border-bottom: 1px solid; - } &:hover { td { background: $hover; @@ -40,7 +27,7 @@ } &.selected { td { - background: #f5f5f5; + background: $background-color; border-top: 1px solid #EEE; border-bottom: 1px solid #EEE; } @@ -52,14 +39,9 @@ .tree-item-file-name { max-width: 320px; vertical-align: middle; - a { - &:hover { - color: $link_hover_color; - } - } - i { - color: $bg_primary; + i, a { + color: $gl-link-color; } img { @@ -79,13 +61,18 @@ .tree_author { padding-right: 8px; + + .commit-author-name { + color: gray; + } } .tree_commit { color: gray; .tree-commit-link { - color: #444; + color: gray; + &:hover { text-decoration: underline; } @@ -111,30 +98,27 @@ background: #f1f1f1; border-left: 1px solid #DDD; } + td.lines { + code { + font-family: $monospace_font; + } + } } } -.tree-download-holder .btn { - padding: 4px 12px; -} - .tree-ref-holder { float: left; - margin-right: 6px; - - .select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice { - padding: 4px 12px; - } + margin-right: 15px; } .readme-holder { - border-top: 1px dashed #CCC; - padding-top: 10px; - .readme-file-title { font-size: 14px; + font-weight: bold; margin-bottom: 20px; color: #777; + border-bottom: 1px solid #DDD; + padding: 10px 0; } } diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss new file mode 100644 index 00000000000..277afa1db9e --- /dev/null +++ b/app/assets/stylesheets/pages/ui_dev_kit.scss @@ -0,0 +1,9 @@ +.gitlab-ui-dev-kit { + > h2 { + font-size: 27px; + border-bottom: 1px solid #CCC; + color: #666; + margin: 30px 0; + font-weight: bold; + } +} diff --git a/app/assets/stylesheets/pages/votes.scss b/app/assets/stylesheets/pages/votes.scss new file mode 100644 index 00000000000..dc9a7d71e8b --- /dev/null +++ b/app/assets/stylesheets/pages/votes.scss @@ -0,0 +1,4 @@ +.votes-inline { + display: inline-block; + margin: 0 8px; +} diff --git a/app/assets/stylesheets/sections/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index dfaeba41cf6..dfaeba41cf6 100644 --- a/app/assets/stylesheets/sections/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index 42dbf4d6ef3..1be0551ad3b 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -11,3 +11,7 @@ header, nav, nav.main-nav, nav.navbar-collapse, nav.navbar-collapse.collapse {di .wiki h1 {font-size: 30px;} .wiki h2 {font-size: 22px;} .wiki h3 {font-size: 18px; font-weight: bold; } + +.sidebar-wrapper { display: none; } +.nav { display: none; } +.btn { display: none; } diff --git a/app/assets/stylesheets/sections/commits.scss b/app/assets/stylesheets/sections/commits.scss deleted file mode 100644 index 684e8377a7b..00000000000 --- a/app/assets/stylesheets/sections/commits.scss +++ /dev/null @@ -1,249 +0,0 @@ -/** - * Commit file - */ -.commit-committer-link, -.commit-author-link { - font-size: 13px; - color: #555; - &:hover { - color: #999; - } -} - -/** COMMIT BLOCK **/ -.commit-title{ - display: block; -} -.commit-title{ - margin-bottom: 10px; -} -.commit-author, .commit-committer{ - display: block; - color: #999; - font-weight: normal; - font-style: italic; -} -.commit-author strong, .commit-committer strong{ - font-weight: bold; - font-style: normal; -} - - -.file-stats a { - color: $style_color; -} - -.file-stats { - .new-file { - a { - color: #090; - } - i { - color: #1BCF00; - } - } - .renamed-file { - i { - color: #FE9300; - } - } - .deleted-file { - a { - color: #B00; - } - i { - color: #EE0000; - } - } - .edit-file{ - i{ - color: #555; - } - } -} - -.label_commit { - @include border-radius(4px); - padding: 2px 4px; - font-size: 13px; - background: #474D57; - color: #fff; - font-family: $monospace_font; -} - - -.commits-compare-switch{ - background: image-url("switch_icon.png") no-repeat center center; - width: 32px; - height: 32px; - text-indent: -9999px; - float: left; - margin-right: 9px; - border: 1px solid #DDD; - @include border-radius(4px); - padding: 4px; - background-color: #EEE; -} - -.commit-description { - background: none; - border: none; - margin: 0; - padding: 0; - margin-top: 10px; -} - -.commit-box { - margin: 10px 0; - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - padding: 20px 0; - - .commit-title { - margin: 0; - font-size: 20px; - } - - .commit-description { - margin-top: 15px; - } -} - - -.commit-stat-summary { - color: #666; - font-size: 14px; - font-weight: normal; - padding: 10px 0; -} - -.commit-info-row { - margin-bottom: 10px; - .avatar { - @extend .avatar-inline; - } - .commit-committer-link, - .commit-author-link { - color: #444; - font-weight: bold; - } -} - -.lists-separator { - margin: 10px 0; - border-top: 1px dashed #CCC; -} - -/** - * COMMIT ROW - */ -li.commit { - .commit-row-title { - font-size: 14px; - margin-bottom: 2px; - - .notes_count { - float: right; - margin-right: 10px; - } - - .commit_short_id { - min-width: 65px; - font-family: $monospace_font; - } - - .str-truncated { - max-width: 70%; - } - - .commit-row-message { - color: #333; - font-weight: 500; - &:hover { - color: #444; - text-decoration: underline; - } - } - - .text-expander { - background: #eee; - color: #555; - padding: 0 5px; - cursor: pointer; - margin-left: 4px; - &:hover { - background-color: #ddd; - } - } - } - - .commit-row-description { - font-size: 14px; - border-left: 1px solid #EEE; - padding: 10px 15px; - margin: 5px 0 10px 5px; - background: #f9f9f9; - display: none; - - pre { - border: none; - background: inherit; - padding: 0; - margin: 0; - } - } - - .commit-row-info { - color: #777; - - a { - color: #777; - } - - .committed_ago { - float: right; - } - } - - &.inline-commit { - .commit-row-title { - font-size: 13px; - } - - .committed_ago { - float: right; - @extend .cgray; - } - } -} - -.commits-feed-holder { - float: right; - .btn { - padding: 4px 12px; - } -} - -.commit-message-container { - background-color: $body-bg; - position: relative; - font-family: $monospace_font; - $left: 12px; - .max-width-marker { - width: 72ch; - color: rgba(0, 0, 0, 0.0); - font-family: inherit; - left: $left; - height: 100%; - border-right: 1px solid mix($input-border, white); - position: absolute; - z-index: 1; - } - > textarea { - background-color: rgba(0, 0, 0, 0.0); - font-family: inherit; - padding-left: $left; - position: relative; - z-index: 2; - } -} diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/sections/header.scss deleted file mode 100644 index 9ad1a1db2cd..00000000000 --- a/app/assets/stylesheets/sections/header.scss +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Application Header - * - */ -header { - &.navbar-gitlab { - margin-bottom: 0; - min-height: 40px; - border: none; - - .navbar-inner { - filter: none; - - .nav > li > a { - font-size: 14px; - line-height: 32px; - padding: 6px 10px; - - &:hover, &:focus, &:active { - background: none; - } - } - - /** NAV block with links and profile **/ - .nav { - float: right; - margin-right: 0; - } - - .navbar-toggle { - color: $style_color; - margin: 0 -15px 0 0; - padding: 10px; - border-radius: 0; - - button i { font-size: 22px; } - - &.collapsed { background-color: transparent !important;} - - &:hover { - background-color: #EEE; - } - } - } - - .turbolink-spinner { - font-size: 20px; - margin-right: 10px; - } - - @media (max-width: $screen-xs-max) { - border-width: 0; - font-size: 18px; - - .app_logo { margin-left: -15px; } - - .title { - @include str-truncated(70%); - } - - .navbar-collapse { - margin-top: 47px; - padding-right: 0; - padding-left: 0; - } - - .navbar-nav { - margin: 5px 0; - - .visible-xs, .visable-sm { - display: table-cell !important; - } - } - - li { - display: table-cell; - width: 1%; - - a { - text-align: center; - font-size: 18px !important; - } - } - } - } - - z-index: 10; - - /** - * - * Logo holder - * - */ - .app_logo { - float: left; - margin-right: 9px; - - a { - float: left; - padding: 0px; - margin: 0 6px; - - h1 { - margin: 0; - background: image-url('logo-black.png') no-repeat center center; - background-size: 32px; - float: left; - height: 46px; - width: 40px; - @include header-font; - text-indent: -9999px; - } - } - &:hover { - background-color: #EEE; - } - } - - /** - * - * Project / Area name - * - */ - .title { - position: relative; - float: left; - margin: 0; - margin-left: 5px; - @include header-font; - @include str-truncated(37%); - } - - .profile-pic { - position: relative; - top: -1px; - padding-right: 0px !important; - img { - width: 26px; - height: 26px; - @include border-radius(4px); - } - } - - /** - * - * Search box - * - */ - .search { - margin-right: 10px; - margin-left: 10px; - margin-top: 8px; - - form { - margin: 0; - padding: 0; - } - - .search-input { - background-image: image-url("icon-search.png"); - background-repeat: no-repeat; - background-position: 10px; - height: inherit; - padding: 4px 6px; - padding-left: 25px; - font-size: 13px; - @include border-radius(3px); - border: 1px solid #c6c6c6; - box-shadow: none; - @include transition(all 0.15s ease-in 0s); - } - } - - - /* - * Dark header - * - */ - &.header-dark { - &.navbar-gitlab { - .navbar-inner { - background: #708090; - border-bottom: 1px solid #AAA; - - .navbar-toggle { color: #fff; } - - .nav > li > a { - color: #AAA; - - &:hover, &:focus, &:active { - background: none; - color: #FFF; - } - } - } - } - - .turbolink-spinner { - color: #FFF; - } - - .search { - .search-input { - background-color: #D2D5DA; - background-color: rgba(255, 255, 255, 0.5); - border: 1px solid #AAA; - - &:focus { - background-color: white; - } - } - } - .search-input::-webkit-input-placeholder { - color: #666; - } - .app_logo { - a { - h1 { - background: image-url('logo-white.png') no-repeat center center; - background-size: 32px; - color: #fff; - } - } - } - .title { - a { - color: #FFF; - &:hover { - text-decoration: underline; - } - } - color: #fff; - } - } - - .app_logo { - .separator { - margin-left: 0; - margin-right: 0; - } - } - - .separator { - float: left; - height: 46px; - width: 2px; - margin-left: 10px; - margin-right: 10px; - } -} - -.search .search-input { - width: 300px; - &:focus { - width: 330px; - } -} - -@media (max-width: 1200px) { - .search .search-input { - width: 200px; - &:focus { - width: 230px; - } - } -} - -@media (max-width: $screen-xs-max) { - #nprogress .spinner { - right: 35px !important; - } -} diff --git a/app/assets/stylesheets/sections/login.scss b/app/assets/stylesheets/sections/login.scss deleted file mode 100644 index 1bcb1f6d68e..00000000000 --- a/app/assets/stylesheets/sections/login.scss +++ /dev/null @@ -1,86 +0,0 @@ -/* Login Page */ -.login-page { - h1 { - font-size: 3em; - font-weight: 200; - } - - .login-box{ - padding: 0 15px; - - .login-heading h3 { - font-weight: 300; - line-height: 2; - } - - .login-footer { - margin-top: 10px; - } - - .btn { - padding: 12px !important; - @extend .btn-block; - } - } - - .brand-image { - img { - max-width: 100%; - margin-bottom: 20px; - } - - &.default-brand-image { - margin: 0 80px; - } - } - - .login-logo { - margin: 10px 0 30px 0; - display: block; - } - - .form-control { - background-color: #F5F5F5; - font-size: 16px; - padding: 14px 10px; - width: 100%; - height: auto; - - &.top { - @include border-radius(5px 5px 0 0); - margin-bottom: 0px; - } - - &.bottom { - @include border-radius(0 0 5px 5px); - border-top: 0; - margin-bottom: 20px; - } - - &.middle { - border-top: 0; - margin-bottom:0px; - @include border-radius(0); - } - - &:active, &:focus { - background-color: #FFF; - } - } - - .login-box a.forgot { - float: right; - padding-top: 6px - } - - .devise-errors { - h2 { - font-size: 14px; - color: #a00; - } - } - - .brand-holder { - border-right: 1px solid #EEE; - } -} diff --git a/app/assets/stylesheets/sections/milestone.scss b/app/assets/stylesheets/sections/milestone.scss deleted file mode 100644 index d20391e38fd..00000000000 --- a/app/assets/stylesheets/sections/milestone.scss +++ /dev/null @@ -1,3 +0,0 @@ -.issues-sortable-list .str-truncated { - max-width: 70%; -} diff --git a/app/assets/stylesheets/sections/nav.scss b/app/assets/stylesheets/sections/nav.scss deleted file mode 100644 index ccd672c5f67..00000000000 --- a/app/assets/stylesheets/sections/nav.scss +++ /dev/null @@ -1,96 +0,0 @@ -.main-nav { - background: #f5f5f5; - margin: 20px 0; - margin-top: 0; - padding-top: 4px; - border-bottom: 1px solid #E9E9E9; - - ul { - padding: 0; - margin: auto; - .count { - font-weight: normal; - display: inline-block; - height: 15px; - padding: 1px 6px; - height: auto; - font-size: 0.82em; - line-height: 14px; - text-align: center; - color: #777; - background: #eee; - @include border-radius(8px); - } - .label { - background: $hover; - text-shadow: none; - color: $style_color; - } - li { - list-style-type: none; - margin: 0; - display: table-cell; - width: 1%; - &.active { - a { - color: $link_color; - font-weight: bold; - border-bottom: 3px solid $link_color; - } - } - - &:hover { - a { - color: $link_hover_color; - border-bottom: 3px solid $link_hover_color; - } - } - } - a { - display: block; - text-align: center; - font-weight: bold; - height: 42px; - line-height: 39px; - color: #777; - text-shadow: 0 1px 1px white; - text-decoration: none; - overflow: hidden; - margin-bottom: -1px; - } - } - - @media (max-width: $screen-xs-max) { - font-size: 18px; - margin: 0; - max-height: none; - - &, .container { - padding: 0; - border-top: 0; - } - - ul { - height: auto; - - li { - display: list-item; - width: auto; - padding: 5px 0; - - &.active { - background-color: $link_hover_color; - - a { - color: #fff; - font-weight: normal; - text-shadow: none; - border: none; - - &:after { display: none; } - } - } - } - } - } -} diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss deleted file mode 100644 index 783f6ae02d3..00000000000 --- a/app/assets/stylesheets/sections/notes.scss +++ /dev/null @@ -1,360 +0,0 @@ -/** - * Notes - */ - -@-webkit-keyframes targe3-note { - from { background:#fffff0; } - 50% { background:#ffffd3; } - to { background:#fffff0; } -} - -ul.notes { - display: block; - list-style: none; - margin: 0px; - padding: 0px; - - .discussion-header, - .note-header { - @extend .cgray; - padding-bottom: 15px; - - a:hover { - text-decoration: none; - } - - .avatar { - float: left; - margin-right: 10px; - } - - .discussion-last-update, - .note-last-update { - &:before { - content: "\00b7"; - } - font-size: 13px; - } - .author { - color: #333; - font-weight: bold; - font-size: 14px; - &:hover { - color: $link_color; - } - } - .author-username { - font-size: 14px; - } - } - - .discussion { - overflow: hidden; - display: block; - position:relative; - } - - .note { - display: block; - position:relative; - .attachment { - font-size: 14px; - } - .note-body { - @include md-typography; - } - .note-header { - padding-bottom: 3px; - } - - &:last-child { - border-bottom: none; - } - } -} - -.diff-file .notes_holder { - font-size: 13px; - line-height: 18px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - - td { - border: 1px solid #ddd; - border-left: none; - - &.notes_line { - text-align: center; - padding: 10px 0; - background: #FFF; - } - &.notes_line2 { - text-align: center; - padding: 10px 0; - border-left: 1px solid #ddd !important; - } - &.notes_content { - background-color: #fff; - border-width: 1px 0; - padding-top: 0; - vertical-align: top; - &.parallel{ - border-width: 1px; - } - } - } -} - -/** - * Actions for Discussions/Notes - */ - -.discussion, -.note { - &.note:hover { - .note-actions { display: block; } - } - .discussion-header:hover { - .discussion-actions { display: block; } - } - - .discussion-actions, - .note-actions { - display: none; - float: right; - - [class~="fa"] { - font-size: 16px; - line-height: 16px; - vertical-align: middle; - } - - a { - @extend .cgray; - - &:hover { - color: $link_hover_color; - &.danger { @extend .cred; } - } - } - } -} -.diff-file .note .note-actions { - right: 0; - top: 0; -} - - -/** - * Line note button on the side of diffs - */ - -.diff-file tr.line_holder { - @mixin show-add-diff-note { - filter: alpha(opacity=100); - opacity: 1.0; - } - - .add-diff-note { - background: image-url("diff_note_add.png") no-repeat left 0; - border: none; - height: 22px; - margin-left: -65px; - position: absolute; - width: 22px; - z-index: 10; - - // "hide" it by default - opacity: 0.0; - filter: alpha(opacity=0); - - &:hover { - @include show-add-diff-note; - } - } - - // "show" the icon also if we just hover somewhere over the line - &:hover > td { - background: $hover !important; - - .add-diff-note { - @include show-add-diff-note; - } - } -} - -/** - * Note Form - */ - -.comment-btn { - @extend .btn-create; -} -.reply-btn { - @extend .btn-primary; -} -.diff-file .diff-content { - tr.line_holder:hover { - &> td.line_content { - background: $hover !important; - border-color: darken($hover, 10%) !important; - } - &> td.new_line, - &> td.old_line { - background: darken($hover, 4%) !important; - border-color: darken($hover, 10%) !important; - } - } - - tr.line_holder:hover > td .line_note_link { - opacity: 1.0; - filter: alpha(opacity=100); - } -} -.diff-file, -.discussion { - .new_note { - margin: 0; - border: none; - } -} -.new_note { - display: none; - .buttons { - float: left; - margin-top: 8px; - } - .clearfix { - margin-bottom: 0; - } - - .note-preview-holder, - .note_text { - background: #FFF; - border: 1px solid #ddd; - min-height: 100px; - padding: 5px; - font-size: 14px; - box-shadow: none; - } - - .note-preview-holder { - > p { - overflow-x: auto; - } - } - - .note_text { - width: 100%; - } - .nav-tabs { - margin-bottom: 0; - border: none; - - li a, - li.active a { - border: 1px solid #DDD; - } - } -} - -/* loading indicator */ -.notes-busy { - margin: 18px; -} - -.note-image-attach { - @extend .col-md-4; - @extend .thumbnail; - margin-left: 45px; - float: none; -} - -.common-note-form { - margin: 0; - background: #F9F9F9; - padding: 5px; - border: 1px solid #DDD; -} - -.note-form-actions { - background: #F9F9F9; - height: 45px; - - .note-form-option { - margin-top: 8px; - margin-left: 30px; - @extend .pull-left; - } - - .js-notify-commit-author { - float: left; - } - - .write-preview-btn { - // makes the "absolute" position for links relative to this - position: relative; - - // preview/edit buttons - > a { - position: absolute; - right: 5px; - top: 8px; - } - } -} - -.note-edit-form { - display: none; - - .note_text { - border: 1px solid #DDD; - box-shadow: none; - font-size: 14px; - height: 80px; - width: 100%; - } - - .form-actions { - padding-left: 20px; - - .btn-save { - float: left; - } - - .note-form-option { - float: left; - padding: 2px 0 0 25px; - } - } -} - -.js-note-attachment-delete { - display: none; -} - -.parallel-comment { - padding: 6px; -} - -.error-alert > .alert { - margin-top: 5px; - margin-bottom: 5px; -} - -.discussion-body, -.diff-file { - .notes .note { - border-color: #ddd; - padding: 10px 15px; - } - - .discussion-reply-holder { - background: #f9f9f9; - padding: 10px 15px; - border-top: 1px solid #DDD; - } -} - -.discussion-notes-count { - font-size: 16px; -} diff --git a/app/assets/stylesheets/sections/profile.scss b/app/assets/stylesheets/sections/profile.scss deleted file mode 100644 index b9f4e317e9c..00000000000 --- a/app/assets/stylesheets/sections/profile.scss +++ /dev/null @@ -1,130 +0,0 @@ -.account-page { - fieldset { - margin-bottom: 15px; - border-bottom: 1px dashed #ddd; - padding-bottom: 15px; - - &:last-child { - border: none; - } - - legend { - border: none; - margin-bottom: 10px; - } - } -} - -.oauth_select_holder { - img { - padding: 2px; - margin-right: 10px; - } - .active { - img { - border: 1px solid #4BD; - background: $hover; - @include border-radius(5px); - } - } -} - -.btn-build-token { - float: left; - padding: 6px 20px; - margin-right: 12px; -} - -.profile-avatar-form-option { - hr { - margin: 10px 0; - } -} - -.user-show-username { - font-weight: 200; - color: #666; -} - -/* - * Appearance settings - * - */ -.themes_opts { - label { - margin-right: 20px; - text-align: center; - - .prev { - height: 80px; - width: 160px; - margin-bottom: 10px; - @include border-radius(4px); - - &.classic { - background: #31363e; - } - - &.default { - background: #f1f1f1; - } - - &.modern { - background: #009871; - } - - &.gray { - background: #373737; - } - - &.violet { - background: #548; - } - } - } -} - -.code_highlight_opts { - margin-top: 10px; - - label { - margin-right: 20px; - text-align: center; - - .prev { - width: 160px; - margin-bottom: 10px; - - img { - max-width: 100%; - @include border-radius(4px); - } - } - } -} - -.profile-groups-avatars { - margin: 0 5px 10px 0; - - img { - width: 50px; - height: 50px; - } -} - -//CSS for password-strength indicator -#password-strength { - margin-bottom: 0; -} - -.has-success input { - background-color: #D6F1D7 !important; -} - -.has-error input { - background-color: #F3CECE !important; -} - -.has-warning input { - background-color: #FFE9A4 !important; -} diff --git a/app/assets/stylesheets/sections/themes.scss b/app/assets/stylesheets/sections/themes.scss deleted file mode 100644 index e69de29bb2d..00000000000 --- a/app/assets/stylesheets/sections/themes.scss +++ /dev/null diff --git a/app/assets/stylesheets/sections/votes.scss b/app/assets/stylesheets/sections/votes.scss deleted file mode 100644 index d683e33e1f0..00000000000 --- a/app/assets/stylesheets/sections/votes.scss +++ /dev/null @@ -1,49 +0,0 @@ -.votes { - font-size: 13px; - line-height: 15px; - .progress { - height: 4px; - margin: 0; - .bar { - float: left; - height: 100%; - } - .bar-success { - @include linear-gradient(#62C462, #51A351); - background-color: #468847; - } - .bar-danger { - @include linear-gradient(#EE5F5B, #BD362F); - background-color: #B94A48; - } - } - .upvotes { - display: inline-block; - color: #468847; - } - .downvotes { - display: inline-block; - color: #B94A48; - } -} -.votes-block { - margin: 6px; - .downvotes { - float: right; - } -} -.votes-inline { - display: inline-block; - margin: 0 8px; -} - -.votes-holder { - float: right; - width: 250px; - - @media (max-width: $screen-xs-max) { - width: 100%; - margin-top: 5px; - margin-bottom: 10px; - } -} diff --git a/app/assets/stylesheets/themes/gitlab-theme.scss b/app/assets/stylesheets/themes/gitlab-theme.scss new file mode 100644 index 00000000000..7cabeaefb93 --- /dev/null +++ b/app/assets/stylesheets/themes/gitlab-theme.scss @@ -0,0 +1,120 @@ +/** + * Styles the GitLab application with a specific color theme + * + * $color-light - + * $color - + * $color-darker - + * $color-dark - + */ +@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { + header { + &.navbar-gitlab { + .header-logo { + background-color: $color-darker; + border-color: $color-darker; + + a { + color: $color-light; + } + + &:hover { + background-color: $color-dark; + a { + color: #FFF; + } + } + } + } + } + + .page-with-sidebar { + .collapse-nav a { + color: #FFF; + background: $color; + } + + .sidebar-wrapper { + background: $color-darker; + border-right: 1px solid $color-darker; + + .sidebar-user { + color: $color-light; + + &:hover { + background-color: $color-dark; + color: #FFF; + text-decoration: none; + } + } + } + + .nav-sidebar li { + a { + color: $color-light; + + &:hover, &:focus, &:active { + background: $color-dark; + } + + i { + color: $color-light; + } + + .count { + color: $color-light; + background: $color-dark; + } + } + + &.separate-item { + border-top: 1px solid $color; + } + + &.active a { + color: #FFF; + font-weight: bold; + + &.no-highlight { + border: none; + } + + i { + color: #FFF + } + } + } + } +} + +$theme-blue: #2980B9; +$theme-charcoal: #474D57; +$theme-graphite: #888888; +$theme-gray: #373737; +$theme-green: #019875; +$theme-violet: #554488; + +body { + &.ui_blue { + @include gitlab-theme(#BECDE9, $theme-blue, #1970A9, #096099); + } + + &.ui_charcoal { + @include gitlab-theme(#979DA7, $theme-charcoal, #373D47, #24272D); + } + + &.ui_graphite { + @include gitlab-theme(#CCCCCC, $theme-graphite, #777777, #666666); + } + + &.ui_gray { + @include gitlab-theme(#979797, $theme-gray, #272727, #222222); + } + + &.ui_green { + @include gitlab-theme(#AADDCC, $theme-green, #018865, #017855); + } + + &.ui_violet { + @include gitlab-theme(#9988CC, $theme-violet, #443366, #332255); + } +} diff --git a/app/assets/stylesheets/themes/ui_basic.scss b/app/assets/stylesheets/themes/ui_basic.scss deleted file mode 100644 index 3e3744fdc33..00000000000 --- a/app/assets/stylesheets/themes/ui_basic.scss +++ /dev/null @@ -1,25 +0,0 @@ -/** - * This file represent some UI that can be changed - * during web app restyle or theme select. - * - */ -.ui_basic { - header { - &.navbar-gitlab { - .navbar-inner { - background: #F1F1F1; - border-bottom: 1px solid #DDD; - .nav > li > a { - color: $style_color; - } - .separator { - background: #F9F9F9; - border-left: 1px solid #DDD; - } - } - } - } - .main-nav { - background: #FFF; - } -} diff --git a/app/assets/stylesheets/themes/ui_color.scss b/app/assets/stylesheets/themes/ui_color.scss deleted file mode 100644 index a08f3ff3d48..00000000000 --- a/app/assets/stylesheets/themes/ui_color.scss +++ /dev/null @@ -1,43 +0,0 @@ -/** - * This file represent some UI that can be changed - * during web app restyle or theme select. - * - * Next items should be placed there - * - link colors - * - header restyles - * - */ -.ui_color { - /* - * Application Header - * - */ - header { - @extend .header-dark; - &.navbar-gitlab { - .navbar-inner { - background: #548; - border-bottom: 1px solid #436; - .app_logo, .navbar-toggle { - &:hover { - background-color: #436; - } - } - .separator { - background: #436; - border-left: 1px solid #659; - } - .nav > li > a { - color: #98C; - } - .search-input { - border-color: #98C; - } - } - } - } - - .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { - background: #659; - } -} diff --git a/app/assets/stylesheets/themes/ui_gray.scss b/app/assets/stylesheets/themes/ui_gray.scss deleted file mode 100644 index 959febad6fe..00000000000 --- a/app/assets/stylesheets/themes/ui_gray.scss +++ /dev/null @@ -1,33 +0,0 @@ -/** - * This file represent some UI that can be changed - * during web app restyle or theme select. - * - * Next items should be placed there - * - link colors - * - header restyles - * - */ -.ui_gray { - /* - * Application Header - * - */ - header { - @extend .header-dark; - &.navbar-gitlab { - .navbar-inner { - background: #373737; - border-bottom: 1px solid #272727; - .app_logo, .navbar-toggle { - &:hover { - background-color: #272727; - } - } - .separator { - background: #272727; - border-left: 1px solid #474747; - } - } - } - } -} diff --git a/app/assets/stylesheets/themes/ui_mars.scss b/app/assets/stylesheets/themes/ui_mars.scss deleted file mode 100644 index 9af5adbf10a..00000000000 --- a/app/assets/stylesheets/themes/ui_mars.scss +++ /dev/null @@ -1,39 +0,0 @@ -/** - * This file represent some UI that can be changed - * during web app restyle or theme select. - * - * Next items should be placed there - * - link colors - * - header restyles - * - */ -.ui_mars { - /* - * Application Header - * - */ - header { - @extend .header-dark; - &.navbar-gitlab { - .navbar-inner { - background: #474D57; - border-bottom: 1px solid #373D47; - .app_logo, .navbar-toggle { - &:hover { - background-color: #373D47; - } - } - .separator { - background: #373D47; - border-left: 1px solid #575D67; - } - .nav > li > a { - color: #979DA7; - } - .search-input { - border-color: #979DA7; - } - } - } - } -} diff --git a/app/assets/stylesheets/themes/ui_modern.scss b/app/assets/stylesheets/themes/ui_modern.scss deleted file mode 100644 index 308a03477db..00000000000 --- a/app/assets/stylesheets/themes/ui_modern.scss +++ /dev/null @@ -1,43 +0,0 @@ -/** - * This file represent some UI that can be changed - * during web app restyle or theme select. - * - * Next items should be placed there - * - link colors - * - header restyles - * - */ -.ui_modern { - /* - * Application Header - * - */ - header { - @extend .header-dark; - &.navbar-gitlab { - .navbar-inner { - background: #019875; - border-bottom: 1px solid #019875; - .app_logo, .navbar-toggle { - &:hover { - background-color: #018865; - } - } - .separator { - background: #018865; - border-left: 1px solid #11A885; - } - .nav > li > a { - color: #ADC; - } - .search-input { - border-color: #8ba; - } - } - } - } - - .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { - background: #019875; - } -} diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index 6a8f20f6047..56e24386463 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -2,8 +2,8 @@ # # Automatically sets the layout and ensures an administrator is logged in class Admin::ApplicationController < ApplicationController + before_action :authenticate_admin! layout 'admin' - before_filter :authenticate_admin! def authenticate_admin! return render_404 unless current_user.is_admin? diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb new file mode 100644 index 00000000000..c7c643db401 --- /dev/null +++ b/app/controllers/admin/application_settings_controller.rb @@ -0,0 +1,52 @@ +class Admin::ApplicationSettingsController < Admin::ApplicationController + before_action :set_application_setting + + def show + end + + def update + if @application_setting.update_attributes(application_setting_params) + redirect_to admin_application_settings_path, + notice: 'Application settings saved successfully' + else + render :show + end + end + + private + + def set_application_setting + @application_setting = ApplicationSetting.current + end + + def application_setting_params + restricted_levels = params[:application_setting][:restricted_visibility_levels] + if restricted_levels.nil? + params[:application_setting][:restricted_visibility_levels] = [] + else + restricted_levels.map! do |level| + level.to_i + end + end + + params.require(:application_setting).permit( + :default_projects_limit, + :default_branch_protection, + :signup_enabled, + :signin_enabled, + :gravatar_enabled, + :twitter_sharing_enabled, + :sign_in_text, + :home_page_url, + :after_sign_out_path, + :max_attachment_size, + :session_expire_delay, + :default_project_visibility, + :default_snippet_visibility, + :restricted_signup_domains_raw, + :version_check_enabled, + :user_oauth_applications, + restricted_visibility_levels: [], + ) + end +end diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb new file mode 100644 index 00000000000..471d24934a0 --- /dev/null +++ b/app/controllers/admin/applications_controller.rb @@ -0,0 +1,52 @@ +class Admin::ApplicationsController < Admin::ApplicationController + before_action :set_application, only: [:show, :edit, :update, :destroy] + + def index + @applications = Doorkeeper::Application.where("owner_id IS NULL") + end + + def show + end + + def new + @application = Doorkeeper::Application.new + end + + def edit + end + + def create + @application = Doorkeeper::Application.new(application_params) + + if @application.save + flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) + redirect_to admin_application_url(@application) + else + render :new + end + end + + def update + if @application.update(application_params) + redirect_to admin_application_path(@application), notice: 'Application was successfully updated.' + else + render :edit + end + end + + def destroy + @application.destroy + redirect_to admin_applications_url, notice: 'Application was successfully destroyed.' + end + + private + + def set_application + @application = Doorkeeper::Application.where("owner_id IS NULL").find(params[:id]) + end + + # Only allow a trusted parameter "white list" through. + def application_params + params[:doorkeeper_application].permit(:name, :redirect_uri) + end +end diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index e1643bb34bf..0808024fc39 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -1,5 +1,5 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController - before_filter :broadcast_messages + before_action :broadcast_messages def index @broadcast_message = BroadcastMessage.new diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index be19139c9b1..c491e5c7550 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -1,7 +1,7 @@ class Admin::DashboardController < Admin::ApplicationController def index - @projects = Project.order("created_at DESC").limit(10) - @users = User.order("created_at DESC").limit(10) - @groups = Group.order("created_at DESC").limit(10) + @projects = Project.limit(10) + @users = User.limit(10) + @groups = Group.limit(10) end end diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb new file mode 100644 index 00000000000..285e8495342 --- /dev/null +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -0,0 +1,44 @@ +class Admin::DeployKeysController < Admin::ApplicationController + before_action :deploy_keys, only: [:index] + before_action :deploy_key, only: [:destroy] + + def index + end + + def new + @deploy_key = deploy_keys.new + end + + def create + @deploy_key = deploy_keys.new(deploy_key_params) + + if @deploy_key.save + redirect_to admin_deploy_keys_path + else + render "new" + end + end + + def destroy + deploy_key.destroy + + respond_to do |format| + format.html { redirect_to admin_deploy_keys_path } + format.json { head :ok } + end + end + + protected + + def deploy_key + @deploy_key ||= deploy_keys.find(params[:id]) + end + + def deploy_keys + @deploy_keys ||= DeployKey.are_public + end + + def deploy_key_params + params.require(:deploy_key).permit(:key, :title) + end +end diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index e6d0c9323c1..4d3e48f7f81 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -1,15 +1,16 @@ class Admin::GroupsController < Admin::ApplicationController - before_filter :group, only: [:edit, :show, :update, :destroy, :project_update, :project_teams_update] + before_action :group, only: [:edit, :show, :update, :destroy, :project_update, :members_update] def index - @groups = Group.order('name ASC') + @groups = Group.all + @groups = @groups.sort(@sort = params[:sort]) @groups = @groups.search(params[:name]) if params[:name].present? - @groups = @groups.page(params[:page]).per(20) + @groups = @groups.page(params[:page]).per(PER_PAGE) end def show - @members = @group.members.order("access_level DESC").page(params[:members_page]).per(30) - @projects = @group.projects.page(params[:projects_page]).per(30) + @members = @group.members.order("access_level DESC").page(params[:members_page]).per(PER_PAGE) + @projects = @group.projects.page(params[:projects_page]).per(PER_PAGE) end def new @@ -21,7 +22,7 @@ class Admin::GroupsController < Admin::ApplicationController def create @group = Group.new(group_params) - @group.path = @group.name.dup.parameterize if @group.name + @group.name = @group.path.dup unless @group.name if @group.save @group.add_owner(current_user) @@ -39,14 +40,14 @@ class Admin::GroupsController < Admin::ApplicationController end end - def project_teams_update - @group.add_users(params[:user_ids].split(','), params[:access_level]) + def members_update + @group.add_users(params[:user_ids].split(','), params[:access_level], current_user) redirect_to [:admin, @group], notice: 'Users were successfully added.' end def destroy - @group.destroy + DestroyGroupService.new(@group, current_user).execute redirect_to admin_groups_path, notice: 'Group was successfully deleted.' end diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index 0a463239d74..690096bdbcf 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -33,7 +33,7 @@ class Admin::HooksController < Admin::ApplicationController owner_name: "Someone", owner_email: "example@gitlabhq.com" } - @hook.execute(data) + @hook.execute(data, 'system_hooks') redirect_to :back end diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb new file mode 100644 index 00000000000..cb33fdd9763 --- /dev/null +++ b/app/controllers/admin/keys_controller.rb @@ -0,0 +1,34 @@ +class Admin::KeysController < Admin::ApplicationController + before_action :user, only: [:show, :destroy] + + def show + @key = user.keys.find(params[:id]) + + respond_to do |format| + format.html + format.js { render nothing: true } + end + end + + def destroy + key = user.keys.find(params[:id]) + + respond_to do |format| + if key.destroy + format.html { redirect_to [:admin, user], notice: 'User key was successfully removed.' } + else + format.html { redirect_to [:admin, user], alert: 'Failed to remove user key.' } + end + end + end + + protected + + def user + @user ||= User.find_by!(username: params[:user_id]) + end + + def key_params + params.require(:user_id, :id) + end +end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 7c2388e81be..f616ccf5684 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,37 +1,40 @@ class Admin::ProjectsController < Admin::ApplicationController - before_filter :project, only: [:show, :transfer] - before_filter :group, only: [:show, :transfer] - before_filter :repository, only: [:show, :transfer] + before_action :project, only: [:show, :transfer] + before_action :group, only: [:show, :transfer] + before_action :repository, only: [:show, :transfer] def index @projects = Project.all - @projects = @projects.where(namespace_id: params[:namespace_id]) if params[:namespace_id].present? + @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? @projects = @projects.where("visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? @projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(20) + @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(PER_PAGE) end def show if @group - @group_members = @group.members.order("access_level DESC").page(params[:group_members_page]).per(30) + @group_members = @group.members.order("access_level DESC").page(params[:group_members_page]).per(PER_PAGE) end - @project_members = @project.project_members.page(params[:project_members_page]).per(30) + @project_members = @project.project_members.page(params[:project_members_page]).per(PER_PAGE) end def transfer ::Projects::TransferService.new(@project, current_user, params.dup).execute - redirect_to [:admin, @project.reload] + @project.reload + redirect_to admin_namespace_project_path(@project.namespace, @project) end protected def project - @project = Project.find_with_namespace(params[:id]) + @project = Project.find_with_namespace( + [params[:namespace_id], '/', params[:id]].join('') + ) @project || render_404 end diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb new file mode 100644 index 00000000000..a62170662e1 --- /dev/null +++ b/app/controllers/admin/services_controller.rb @@ -0,0 +1,45 @@ +class Admin::ServicesController < Admin::ApplicationController + before_action :service, only: [:edit, :update] + + def index + @services = services_templates + end + + def edit + unless service.present? + redirect_to admin_application_settings_services_path, + alert: "Service is unknown or it doesn't exist" + end + end + + def update + if service.update_attributes(application_services_params[:service]) + redirect_to admin_application_settings_services_path, + notice: 'Application settings saved successfully' + else + render :edit + end + end + + private + + def services_templates + templates = [] + + Service.available_services_names.each do |service_name| + service_template = service_name.concat("_service").camelize.constantize + templates << service_template.where(template: true).first_or_create + end + + templates + end + + def service + @service ||= Service.where(id: params[:id], template: true).first + end + + def application_services_params + params.permit(:id, + service: Projects::ServicesController::ALLOWED_PARAMS) + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index baad9095b70..06d6d61e907 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,16 +1,17 @@ class Admin::UsersController < Admin::ApplicationController - before_filter :user, only: [:show, :edit, :update, :destroy] + before_action :user, only: [:show, :edit, :update, :destroy] def index - @users = User.filter(params[:filter]) + @users = User.order_name_asc.filter(params[:filter]) @users = @users.search(params[:name]) if params[:name].present? @users = @users.sort(@sort = params[:sort]) - @users = @users.alphabetically.page(params[:page]) + @users = @users.page(params[:page]) end def show @personal_projects = user.personal_projects @joined_projects = user.projects.joined(@user) + @keys = user.keys end def new @@ -23,7 +24,7 @@ class Admin::UsersController < Admin::ApplicationController def block if user.block - redirect_to :back, alert: "Successfully blocked" + redirect_to :back, notice: "Successfully blocked" else redirect_to :back, alert: "Error occurred. User was not blocked" end @@ -31,7 +32,7 @@ class Admin::UsersController < Admin::ApplicationController def unblock if user.activate - redirect_to :back, alert: "Successfully unblocked" + redirect_to :back, notice: "Successfully unblocked" else redirect_to :back, alert: "Error occurred. User was not unblocked" end @@ -71,8 +72,8 @@ class Admin::UsersController < Admin::ApplicationController end respond_to do |format| + user.skip_reconfirmation! if user.update_attributes(user_params_with_pass) - user.confirm! format.html { redirect_to [:admin, user], notice: 'User was successfully updated.' } format.json { head :ok } else @@ -85,11 +86,7 @@ class Admin::UsersController < Admin::ApplicationController end def destroy - # 1. Remove groups where user is the only owner - user.solo_owned_groups.map(&:destroy) - - # 2. Remove user with all authored content including personal projects - user.destroy + DeleteUserService.new.execute(user) respond_to do |format| format.html { redirect_to admin_users_path } @@ -101,6 +98,8 @@ class Admin::UsersController < Admin::ApplicationController email = user.emails.find(params[:email_id]) email.destroy + user.update_secondary_emails! + respond_to do |format| format.html { redirect_to :back, notice: "Successfully removed email." } format.js { render nothing: true } @@ -117,8 +116,8 @@ class Admin::UsersController < Admin::ApplicationController params.require(:user).permit( :email, :remember_me, :bio, :name, :username, :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password, - :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, - :projects_limit, :can_create_group, :admin + :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password, + :projects_limit, :can_create_group, :admin, :key_id ) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f1e1bebe5ce..a657d3c54ee 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,19 +1,26 @@ require 'gon' class ApplicationController < ActionController::Base - before_filter :authenticate_user_from_token! - before_filter :authenticate_user! - before_filter :reject_blocked! - before_filter :check_password_expiration - before_filter :ldap_security_check - before_filter :default_headers - before_filter :add_gon_variables - before_filter :configure_permitted_parameters, if: :devise_controller? - before_filter :require_email, unless: :devise_controller? + include Gitlab::CurrentSettings + include GitlabRoutingHelper + include PageLayoutHelper + + PER_PAGE = 20 + + before_action :authenticate_user_from_token! + before_action :authenticate_user! + before_action :reject_blocked! + before_action :check_password_expiration + before_action :ldap_security_check + before_action :default_headers + before_action :add_gon_variables + before_action :configure_permitted_parameters, if: :devise_controller? + before_action :require_email, unless: :devise_controller? protect_from_forgery with: :exception - helper_method :abilities, :can? + helper_method :abilities, :can?, :current_application_settings + helper_method :github_import_enabled?, :gitlab_import_enabled?, :bitbucket_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -46,6 +53,17 @@ class ApplicationController < ActionController::Base end end + def authenticate_user!(*args) + # If user is not signed-in and tries to access root_path - redirect him to landing page + if current_application_settings.home_page_url.present? + if current_user.nil? && controller_name == 'dashboard' && action_name == 'show' + redirect_to current_application_settings.home_page_url and return + end + end + + super(*args) + end + def log_exception(exception) application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace application_trace.map!{ |t| " #{t}\n" } @@ -70,6 +88,10 @@ class ApplicationController < ActionController::Base end end + def after_sign_out_path_for(resource) + current_application_settings.after_sign_out_path || new_user_session_path + end + def abilities Ability.abilities end @@ -80,6 +102,7 @@ class ApplicationController < ActionController::Base def project unless @project + namespace = params[:namespace_id] id = params[:project_id] || params[:id] # Redirect from @@ -91,7 +114,7 @@ class ApplicationController < ActionController::Base redirect_to request.original_url.gsub(/\.git\Z/, '') and return end - @project = Project.find_with_namespace(id) + @project = Project.find_with_namespace("#{namespace}/#{id}") if @project and can?(current_user, :read_project, @project) @project @@ -108,7 +131,8 @@ class ApplicationController < ActionController::Base def repository @repository ||= project.repository - rescue Grit::NoSuchPathError + rescue Grit::NoSuchPathError => e + log_exception(e) nil end @@ -116,11 +140,6 @@ class ApplicationController < ActionController::Base return access_denied! unless can?(current_user, action, project) end - def authorize_labels! - # Labels should be accessible for issues and/or merge requests - authorize_read_issue! || authorize_read_merge_request! - end - def access_denied! render "errors/access_denied", layout: "errors", status: 404 end @@ -134,7 +153,7 @@ class ApplicationController < ActionController::Base end def method_missing(method_sym, *arguments, &block) - if method_sym.to_s =~ /^authorize_(.*)!$/ + if method_sym.to_s =~ /\Aauthorize_(.*)!\z/ authorize_project!($1.to_sym) else super @@ -168,10 +187,11 @@ class ApplicationController < ActionController::Base end def add_gon_variables - gon.default_issues_tracker = Project.issues_tracker.default_value + gon.default_issues_tracker = Project.new.default_issue_tracker.to_param gon.api_version = API::API.version gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s + gon.max_file_size = current_application_settings.max_attachment_size; if current_user gon.current_user_id = current_user.id @@ -227,7 +247,7 @@ class ApplicationController < ActionController::Base end def configure_permitted_parameters - devise_parameter_sanitizer.sanitize(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me) } + devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me, :otp_attempt) } end def hexdigest(string) @@ -239,4 +259,50 @@ class ApplicationController < ActionController::Base redirect_to profile_path, notice: 'Please complete your profile with email address' and return end end + + def set_filters_params + params[:sort] ||= 'created_desc' + params[:scope] = 'all' if params[:scope].blank? + params[:state] = 'opened' if params[:state].blank? + + @filter_params = params.dup + + if @project + @filter_params[:project_id] = @project.id + elsif @group + @filter_params[:group_id] = @group.id + else + # TODO: this filter ignore issues/mr created in public or + # 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 + end + + @filter_params + end + + def get_issues_collection + set_filters_params + @issuable_finder = IssuesFinder.new(current_user, @filter_params) + @issuable_finder.execute + end + + def get_merge_requests_collection + set_filters_params + @issuable_finder = MergeRequestsFinder.new(current_user, @filter_params) + @issuable_finder.execute + end + + def github_import_enabled? + OauthHelper.enabled_oauth_providers.include?(:github) + end + + def gitlab_import_enabled? + OauthHelper.enabled_oauth_providers.include?(:gitlab) + end + + def bitbucket_import_enabled? + OauthHelper.enabled_oauth_providers.include?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? + end end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb new file mode 100644 index 00000000000..11af9895261 --- /dev/null +++ b/app/controllers/autocomplete_controller.rb @@ -0,0 +1,30 @@ +class AutocompleteController < ApplicationController + def users + @users = + if params[:project_id].present? + project = Project.find(params[:project_id]) + + if can?(current_user, :read_project, project) + project.team.users + end + elsif params[:group_id] + group = Group.find(params[:group_id]) + + if can?(current_user, :read_group, group) + group.users + end + else + User.all + end + + @users = @users.search(params[:search]) if params[:search].present? + @users = @users.active + @users = @users.page(params[:page]).per(PER_PAGE) + render json: @users, only: [:name, :username, :id], methods: [:avatar_url] + end + + def user + @user = User.find(params[:id]) + render json: @user, only: [:name, :username, :id], methods: [:avatar_url] + end +end diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb new file mode 100644 index 00000000000..d5918a7af3b --- /dev/null +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -0,0 +1,30 @@ +# == AuthenticatesWithTwoFactor +# +# Controller concern to handle two-factor authentication +# +# Upon inclusion, skips `require_no_authentication` on `:create`. +module AuthenticatesWithTwoFactor + extend ActiveSupport::Concern + + included do + # This action comes from DeviseController, but because we call `sign_in` + # manually, not skipping this action would cause a "You are already signed + # in." error message to be shown upon successful login. + skip_before_action :require_no_authentication, only: [:create] + end + + # Store the user's ID in the session for later retrieval and render the + # two factor code prompt + # + # The user must have been authenticated with a valid login and password + # before calling this method! + # + # user - User record + # + # Returns nil + def prompt_for_two_factor(user) + session[:otp_user_id] = user.id + + render 'devise/sessions/two_factor' and return + end +end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index bc98eab133c..af1faca93f6 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -4,11 +4,11 @@ class ConfirmationsController < Devise::ConfirmationsController def after_confirmation_path_for(resource_name, resource) if signed_in?(resource_name) - signed_in_root_path(resource) + after_sign_in_path_for(resource) else sign_in(resource) if signed_in?(resource_name) - signed_in_root_path(resource) + after_sign_in_path_for(resource) else new_session_path(resource_name) end diff --git a/app/controllers/dashboard/application_controller.rb b/app/controllers/dashboard/application_controller.rb new file mode 100644 index 00000000000..962ea38d6c9 --- /dev/null +++ b/app/controllers/dashboard/application_controller.rb @@ -0,0 +1,3 @@ +class Dashboard::ApplicationController < ApplicationController + layout 'dashboard' +end diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb new file mode 100644 index 00000000000..3bc94ff2187 --- /dev/null +++ b/app/controllers/dashboard/groups_controller.rb @@ -0,0 +1,5 @@ +class Dashboard::GroupsController < Dashboard::ApplicationController + def index + @group_members = current_user.group_members.page(params[:page]).per(PER_PAGE) + end +end diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb new file mode 100644 index 00000000000..53896d4f2c7 --- /dev/null +++ b/app/controllers/dashboard/milestones_controller.rb @@ -0,0 +1,34 @@ +class Dashboard::MilestonesController < Dashboard::ApplicationController + before_action :load_projects + + def index + project_milestones = case params[:state] + when 'all'; state + when 'closed'; state('closed') + else state('active') + end + @dashboard_milestones = Milestones::GroupService.new(project_milestones).execute + @dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(PER_PAGE) + end + + def show + project_milestones = Milestone.where(project_id: @projects).order("due_date ASC") + @dashboard_milestone = Milestones::GroupService.new(project_milestones).milestone(title) + end + + private + + def load_projects + @projects = current_user.authorized_projects.sorted_by_activity.non_archived + end + + def title + params[:title] + end + + def state(state = nil) + conditions = { project_id: @projects } + conditions.reverse_merge!(state: state) if state + Milestone.where(conditions).order("title ASC") + end +end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb new file mode 100644 index 00000000000..da96171e885 --- /dev/null +++ b/app/controllers/dashboard/projects_controller.rb @@ -0,0 +1,27 @@ +class Dashboard::ProjectsController < Dashboard::ApplicationController + before_action :event_filter + + def starred + @projects = current_user.starred_projects + @projects = @projects.includes(:namespace, :forked_from_project, :tags) + @projects = @projects.sort(@sort = params[:sort]) + @groups = [] + + respond_to do |format| + format.html + + format.json do + load_events + pager_json("events/_events", @events.count) + end + end + end + + private + + def load_events + @events = Event.in_projects(@projects.pluck(:id)) + @events = @event_filter.apply_filter(@events).with_associations + @events = @events.limit(20).offset(params[:offset] || 0) + end +end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 5aff526d1b5..d2f0c43929f 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,68 +1,37 @@ -class DashboardController < ApplicationController - respond_to :html - - before_filter :load_projects, except: [:projects] - before_filter :event_filter, only: :show - before_filter :default_filter, only: [:issues, :merge_requests] +class DashboardController < Dashboard::ApplicationController + before_action :load_projects + before_action :event_filter, only: :show + respond_to :html def show - # Fetch only 30 projects. - # If user needs more - point to Dashboard#projects page - @projects_limit = 30 - - @groups = current_user.authorized_groups.sort_by(&:human_name) - @has_authorized_projects = @projects.count > 0 - @projects_count = @projects.count - @projects = @projects.limit(@projects_limit) - - @events = Event.in_projects(current_user.authorized_projects.pluck(:id)) - @events = @event_filter.apply_filter(@events) - @events = @events.limit(20).offset(params[:offset] || 0) - + @projects = @projects.includes(:namespace) @last_push = current_user.recent_push - @publicish_project_count = Project.publicish(current_user).count - respond_to do |format| format.html - format.json { pager_json("events/_events", @events.count) } - format.atom { render layout: false } - end - end - def projects - @projects = case params[:scope] - when 'personal' then - current_user.namespace.projects - when 'joined' then - current_user.authorized_projects.joined(current_user) - when 'owned' then - current_user.owned_projects - else - current_user.authorized_projects - end + format.json do + load_events + pager_json("events/_events", @events.count) + end - @projects = @projects.where(namespace_id: Group.find_by(name: params[:group])) if params[:group].present? - @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? - @projects = @projects.includes(:namespace) - @projects = @projects.tagged_with(params[:tag]) if params[:tag].present? - @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.page(params[:page]).per(30) - - @tags = current_user.authorized_projects.tags_on(:tags) - @groups = current_user.authorized_groups + format.atom do + load_events + render layout: false + end + end end def merge_requests - @merge_requests = MergeRequestsFinder.new.execute(current_user, params) - @merge_requests = @merge_requests.page(params[:page]).per(20) + @merge_requests = get_merge_requests_collection + @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) @merge_requests = @merge_requests.preload(:author, :target_project) end def issues - @issues = IssuesFinder.new.execute(current_user, params) - @issues = @issues.page(params[:page]).per(20) + @issues = get_issues_collection + @issues = @issues.page(params[:page]).per(PER_PAGE) @issues = @issues.preload(:author, :project) respond_to do |format| @@ -77,9 +46,9 @@ class DashboardController < ApplicationController @projects = current_user.authorized_projects.sorted_by_activity.non_archived end - def default_filter - params[:scope] = 'assigned-to-me' if params[:scope].blank? - params[:state] = 'opened' if params[:state].blank? - params[:authorized_only] = true + def load_events + @events = Event.in_projects(current_user.authorized_projects.pluck(:id)) + @events = @event_filter.apply_filter(@events).with_associations + @events = @events.limit(20).offset(params[:offset] || 0) end end diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb new file mode 100644 index 00000000000..4b275033d26 --- /dev/null +++ b/app/controllers/explore/application_controller.rb @@ -0,0 +1,3 @@ +class Explore::ApplicationController < ApplicationController + layout 'explore' +end diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb index ada7031fea4..55cda0cff17 100644 --- a/app/controllers/explore/groups_controller.rb +++ b/app/controllers/explore/groups_controller.rb @@ -1,13 +1,11 @@ -class Explore::GroupsController < ApplicationController - skip_before_filter :authenticate_user!, +class Explore::GroupsController < Explore::ApplicationController + skip_before_action :authenticate_user!, :reject_blocked, :set_current_user_for_observers - layout "explore" - def index @groups = GroupsFinder.new.execute(current_user) @groups = @groups.search(params[:search]) if params[:search].present? @groups = @groups.sort(@sort = params[:sort]) - @groups = @groups.page(params[:page]).per(20) + @groups = @groups.page(params[:page]).per(PER_PAGE) end end diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index d75fd8e72fa..e9bcb44f6b3 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -1,24 +1,25 @@ -class Explore::ProjectsController < ApplicationController - skip_before_filter :authenticate_user!, +class Explore::ProjectsController < Explore::ApplicationController + skip_before_action :authenticate_user!, :reject_blocked - layout 'explore' - def index @projects = ProjectsFinder.new.execute(current_user) + @tags = @projects.tags_on(:tags) + @projects = @projects.tagged_with(params[:tag]) if params[:tag].present? + @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.search(params[:search]) if params[:search].present? @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.includes(:namespace).page(params[:page]).per(20) + @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) end def trending @trending_projects = TrendingProjectsFinder.new.execute(current_user) - @trending_projects = @trending_projects.page(params[:page]).per(10) + @trending_projects = @trending_projects.page(params[:page]).per(PER_PAGE) end def starred @starred_projects = ProjectsFinder.new.execute(current_user) - @starred_projects = @starred_projects.order('star_count DESC') - @starred_projects = @starred_projects.page(params[:page]).per(10) + @starred_projects = @starred_projects.reorder('star_count DESC') + @starred_projects = @starred_projects.page(params[:page]).per(PER_PAGE) end end diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb deleted file mode 100644 index 7937454810d..00000000000 --- a/app/controllers/files_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -class FilesController < ApplicationController - def download - note = Note.find(params[:id]) - uploader = note.attachment - - if uploader.file_storage? - if can?(current_user, :read_project, note.project) - send_file uploader.file.path, disposition: 'attachment' - else - not_found! - end - else - redirect_to uploader.url - end - end -end diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb new file mode 100644 index 00000000000..4df9d1b7533 --- /dev/null +++ b/app/controllers/groups/application_controller.rb @@ -0,0 +1,21 @@ +class Groups::ApplicationController < ApplicationController + layout 'group' + + private + + def authorize_read_group! + unless @group and can?(current_user, :read_group, @group) + if current_user.nil? + return authenticate_user! + else + return render_404 + end + end + end + + def authorize_admin_group! + unless can?(current_user, :admin_group, group) + return render_404 + end + end +end diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb index 38071410f40..6aa64222f77 100644 --- a/app/controllers/groups/avatars_controller.rb +++ b/app/controllers/groups/avatars_controller.rb @@ -1,6 +1,4 @@ class Groups::AvatarsController < ApplicationController - layout "profile" - def destroy @group = Group.find_by(path: params[:group_id]) @group.remove_avatar! diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index ca88d033878..040255f08e6 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,15 +1,29 @@ -class Groups::GroupMembersController < ApplicationController - before_filter :group +class Groups::GroupMembersController < Groups::ApplicationController + skip_before_action :authenticate_user!, only: [:index] + before_action :group # Authorize - before_filter :authorize_admin_group! + before_action :authorize_read_group! + before_action :authorize_admin_group!, except: [:index, :leave] - layout 'group' + def index + @project = @group.projects.find(params[:project_id]) if params[:project_id] + @members = @group.group_members + @members = @members.non_invite unless can?(current_user, :admin_group, @group) + + if params[:search].present? + users = @group.users.search(params[:search]).to_a + @members = @members.where(user_id: users) + end + + @members = @members.order('access_level DESC').page(params[:page]).per(50) + @group_member = GroupMember.new + end def create - @group.add_users(params[:user_ids].split(','), params[:access_level]) + @group.add_users(params[:user_ids].split(','), params[:access_level], current_user) - redirect_to members_group_path(@group), notice: 'Users were successfully added.' + redirect_to group_group_members_path(@group), notice: 'Users were successfully added.' end def update @@ -18,12 +32,12 @@ class Groups::GroupMembersController < ApplicationController end def destroy - @users_group = @group.group_members.find(params[:id]) + @group_member = @group.group_members.find(params[:id]) - if can?(current_user, :destroy, @users_group) # May fail if last owner. - @users_group.destroy + if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner. + @group_member.destroy respond_to do |format| - format.html { redirect_to members_group_path(@group), notice: 'User was successfully removed from group.' } + format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } format.js { render nothing: true } end else @@ -31,18 +45,41 @@ class Groups::GroupMembersController < ApplicationController end end - protected + def resend_invite + redirect_path = group_group_members_path(@group) - def group - @group ||= Group.find_by(path: params[:group_id]) + @group_member = @group.group_members.find(params[:id]) + + if @group_member.invite? + @group_member.resend_invite + + redirect_to redirect_path, notice: 'The invitation was successfully resent.' + else + redirect_to redirect_path, alert: 'The invitation has already been accepted.' + end end - def authorize_admin_group! - unless can?(current_user, :manage_group, group) - return render_404 + def leave + @group_member = @group.group_members.where(user_id: current_user.id).first + + if can?(current_user, :destroy_group_member, @group_member) + @group_member.destroy + redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.") + else + if @group.last_owner?(current_user) + redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.") + else + return render_403 + end end end + protected + + def group + @group ||= Group.find_by(path: params[:group_id]) + end + def member_params params.require(:group_member).permit(:access_level, :user_id) end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 860d8e03922..669f7f3126d 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -1,16 +1,14 @@ -class Groups::MilestonesController < ApplicationController - layout 'group' - - before_filter :authorize_group_milestone!, only: :update +class Groups::MilestonesController < Groups::ApplicationController + before_action :authorize_group_milestone!, only: :update def index - project_milestones = case params[:status] - when 'all'; status - when 'closed'; status('closed') - else status('active') + project_milestones = case params[:state] + when 'all'; state + when 'closed'; state('closed') + else state('active') end @group_milestones = Milestones::GroupService.new(project_milestones).execute - @group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(30) + @group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(PER_PAGE) end def show @@ -44,13 +42,13 @@ class Groups::MilestonesController < ApplicationController params[:title] end - def status(state = nil) + def state(state = nil) conditions = { project_id: group.projects } conditions.reverse_merge!(state: state) if state Milestone.where(conditions).order("title ASC") end def authorize_group_milestone! - return render_404 unless can?(current_user, :manage_group, group) + return render_404 unless can?(current_user, :admin_group, group) end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 36222758eb2..2e381822e42 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,29 +1,26 @@ -class GroupsController < ApplicationController - skip_before_filter :authenticate_user!, only: [:show, :issues, :members, :merge_requests] +class GroupsController < Groups::ApplicationController + skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests] respond_to :html - before_filter :group, except: [:new, :create] + before_action :group, except: [:new, :create] # Authorize - before_filter :authorize_read_group!, except: [:new, :create] - before_filter :authorize_admin_group!, only: [:edit, :update, :destroy, :projects] - before_filter :authorize_create_group!, only: [:new, :create] + before_action :authorize_read_group!, except: [:new, :create] + before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects] + before_action :authorize_create_group!, only: [:new, :create] # Load group projects - before_filter :load_projects, except: [:new, :create, :projects, :edit, :update] - - before_filter :default_filter, only: [:issues, :merge_requests] + before_action :load_projects, except: [:new, :create, :projects, :edit, :update] + before_action :event_filter, only: :show layout :determine_layout - before_filter :set_title, only: [:new, :create] - def new @group = Group.new end def create @group = Group.new(group_params) - @group.path = @group.name.dup.parameterize if @group.name + @group.name = @group.path.dup unless @group.name if @group.save @group.add_owner(current_user) @@ -34,27 +31,33 @@ class GroupsController < ApplicationController end def show - @events = Event.in_projects(project_ids) - @events = event_filter.apply_filter(@events) - @events = @events.limit(20).offset(params[:offset] || 0) @last_push = current_user.recent_push if current_user + @projects = @projects.includes(:namespace) respond_to do |format| format.html - format.json { pager_json("events/_events", @events.count) } - format.atom { render layout: false } + + format.json do + load_events + pager_json("events/_events", @events.count) + end + + format.atom do + load_events + render layout: false + end end end def merge_requests - @merge_requests = MergeRequestsFinder.new.execute(current_user, params) - @merge_requests = @merge_requests.page(params[:page]).per(20) + @merge_requests = get_merge_requests_collection + @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) @merge_requests = @merge_requests.preload(:author, :target_project) end def issues - @issues = IssuesFinder.new.execute(current_user, params) - @issues = @issues.page(params[:page]).per(20) + @issues = get_issues_collection + @issues = @issues.page(params[:page]).per(PER_PAGE) @issues = @issues.preload(:author, :project) respond_to do |format| @@ -63,19 +66,6 @@ class GroupsController < ApplicationController end end - def members - @project = group.projects.find(params[:project_id]) if params[:project_id] - @members = group.group_members - - if params[:search].present? - users = group.users.search(params[:search]).to_a - @members = @members.where(user_id: users) - end - - @members = @members.order('access_level DESC').page(params[:page]).per(50) - @users_group = GroupMember.new - end - def edit end @@ -92,7 +82,7 @@ class GroupsController < ApplicationController end def destroy - @group.destroy + DestroyGroupService.new(@group, current_user).execute redirect_to root_path, notice: 'Group was removed.' end @@ -128,39 +118,21 @@ class GroupsController < ApplicationController end end - def authorize_admin_group! - unless can?(current_user, :manage_group, group) - return render_404 - end - end - - def set_title - @title = 'New Group' - end - def determine_layout if [:new, :create].include?(action_name.to_sym) - 'navless' - elsif current_user - 'group' + 'application' else - 'public_group' - end - end - - def default_filter - if params[:scope].blank? - if current_user - params[:scope] = 'assigned-to-me' - else - params[:scope] = 'all' - end + 'group' end - params[:state] = 'opened' if params[:state].blank? - params[:group_id] = @group.id end def group_params params.require(:group).permit(:name, :description, :path, :avatar) end + + def load_events + @events = Event.in_projects(project_ids) + @events = event_filter.apply_filter(@events).with_associations + @events = @events.limit(20).offset(params[:offset] || 0) + end end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index fc498559d6b..8a45dc8860d 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -1,18 +1,86 @@ class HelpController < ApplicationController + layout 'help' + def index end def show - @category = params[:category] - @file = params[:file] + @category = clean_path_info(path_params[:category]) + @file = path_params[:file] + + respond_to do |format| + format.any(:markdown, :md, :html) do + path = Rails.root.join('doc', @category, "#{@file}.md") + + if File.exist?(path) + @markdown = File.read(path) + + render 'show.html.haml' + else + # Force template to Haml + render 'errors/not_found.html.haml', layout: 'errors', status: 404 + end + end + + # Allow access to images in the doc folder + format.any(:png, :gif, :jpeg) do + path = Rails.root.join('doc', @category, "#{@file}.#{params[:format]}") + + if File.exist?(path) + send_file(path, disposition: 'inline') + else + head :not_found + end + end - if File.exists?(Rails.root.join('doc', @category, @file + '.md')) - render 'show' - else - not_found! + # Any other format we don't recognize, just respond 404 + format.any { head :not_found } end end def shortcuts end + + def ui + end + + private + + def path_params + params.require(:category) + params.require(:file) + + params + end + + PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact) + + # Taken from ActionDispatch::FileHandler + # Cleans up the path, to prevent directory traversal outside the doc folder. + def clean_path_info(path_info) + parts = path_info.split(PATH_SEPS) + + clean = [] + + # Walk over each part of the path + parts.each do |part| + # Turn `one//two` or `one/./two` into `one/two`. + next if part.empty? || part == '.' + + if part == '..' + # Turn `one/two/../` into `one` + clean.pop + else + # Add simple folder names to the clean path. + clean << part + end + end + + # If the path was an absolute path (i.e. `/` or `/one/two`), + # add `/` to the front of the clean path. + clean.unshift '/' if parts.empty? || parts.first.empty? + + # Join all the clean path parts by the path separator. + ::File.join(*clean) + end end diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb new file mode 100644 index 00000000000..93a7ace3530 --- /dev/null +++ b/app/controllers/import/base_controller.rb @@ -0,0 +1,19 @@ +class Import::BaseController < ApplicationController + + private + + def get_or_create_namespace + begin + namespace = Group.create!(name: @target_namespace, path: @target_namespace, owner: current_user) + namespace.add_owner(current_user) + rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid + namespace = Namespace.find_by_path_or_name(@target_namespace) + unless current_user.can?(:create_projects, namespace) + @already_been_taken = true + return false + end + end + + namespace + end +end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb new file mode 100644 index 00000000000..ca78a4aaa8e --- /dev/null +++ b/app/controllers/import/bitbucket_controller.rb @@ -0,0 +1,82 @@ +class Import::BitbucketController < Import::BaseController + before_action :verify_bitbucket_import_enabled + before_action :bitbucket_auth, except: :callback + + rescue_from OAuth::Error, with: :bitbucket_unauthorized + + def callback + request_token = session.delete(:oauth_request_token) + raise "Session expired!" if request_token.nil? + + request_token.symbolize_keys! + + access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url) + + current_user.bitbucket_access_token = access_token.token + current_user.bitbucket_access_token_secret = access_token.secret + + current_user.save + redirect_to status_import_bitbucket_url + end + + def status + @repos = client.projects + + @already_added_projects = current_user.created_projects.where(import_type: "bitbucket") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "bitbucket").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @repo_id = params[:repo_id] || "" + repo = client.project(@repo_id.gsub("___", "/")) + @project_name = repo["slug"] + + repo_owner = repo["owner"] + repo_owner = current_user.username if repo_owner == client.user["user"]["username"] + @target_namespace = params[:new_namespace].presence || repo_owner + + namespace = get_or_create_namespace || (render and return) + + unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user).execute + @access_denied = true + render + return + end + + @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, current_user).execute + end + + private + + def client + @client ||= Gitlab::BitbucketImport::Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret) + end + + def verify_bitbucket_import_enabled + not_found! unless bitbucket_import_enabled? + end + + def bitbucket_auth + if current_user.bitbucket_access_token.blank? + go_to_bitbucket_for_permissions + end + end + + def go_to_bitbucket_for_permissions + request_token = client.request_token(callback_import_bitbucket_url) + session[:oauth_request_token] = request_token + + redirect_to client.authorize_url(request_token, callback_import_bitbucket_url) + end + + def bitbucket_unauthorized + go_to_bitbucket_for_permissions + end +end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb new file mode 100644 index 00000000000..b9f99c1b88a --- /dev/null +++ b/app/controllers/import/github_controller.rb @@ -0,0 +1,68 @@ +class Import::GithubController < Import::BaseController + before_action :verify_github_import_enabled + before_action :github_auth, except: :callback + + rescue_from Octokit::Unauthorized, with: :github_unauthorized + + def callback + token = client.get_token(params[:code]) + current_user.github_access_token = token + current_user.save + redirect_to status_import_github_url + end + + def status + @repos = client.repos + client.orgs.each do |org| + @repos += client.org_repos(org.login) + end + + @already_added_projects = current_user.created_projects.where(import_type: "github") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos.reject!{ |repo| already_added_projects_names.include? repo.full_name } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "github").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @repo_id = params[:repo_id].to_i + repo = client.repo(@repo_id) + @project_name = repo.name + + repo_owner = repo.owner.login + repo_owner = current_user.username if repo_owner == client.user.login + @target_namespace = params[:new_namespace].presence || repo_owner + + namespace = get_or_create_namespace || (render and return) + + @project = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, current_user).execute + end + + private + + def client + @client ||= Gitlab::GithubImport::Client.new(current_user.github_access_token) + end + + def verify_github_import_enabled + not_found! unless github_import_enabled? + end + + def github_auth + if current_user.github_access_token.blank? + go_to_github_for_permissions + end + end + + def go_to_github_for_permissions + redirect_to client.authorize_url(callback_import_github_url) + end + + def github_unauthorized + go_to_github_for_permissions + end +end diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb new file mode 100644 index 00000000000..1b8962d8924 --- /dev/null +++ b/app/controllers/import/gitlab_controller.rb @@ -0,0 +1,65 @@ +class Import::GitlabController < Import::BaseController + before_action :verify_gitlab_import_enabled + before_action :gitlab_auth, except: :callback + + rescue_from OAuth2::Error, with: :gitlab_unauthorized + + def callback + token = client.get_token(params[:code], callback_import_gitlab_url) + current_user.gitlab_access_token = token + current_user.save + redirect_to status_import_gitlab_url + end + + def status + @repos = client.projects + + @already_added_projects = current_user.created_projects.where(import_type: "gitlab") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos = @repos.to_a.reject{ |repo| already_added_projects_names.include? repo["path_with_namespace"] } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "gitlab").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @repo_id = params[:repo_id].to_i + repo = client.project(@repo_id) + @project_name = repo["name"] + + repo_owner = repo["namespace"]["path"] + repo_owner = current_user.username if repo_owner == client.user["username"] + @target_namespace = params[:new_namespace].presence || repo_owner + + namespace = get_or_create_namespace || (render and return) + + @project = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, current_user).execute + end + + private + + def client + @client ||= Gitlab::GitlabImport::Client.new(current_user.gitlab_access_token) + end + + def verify_gitlab_import_enabled + not_found! unless gitlab_import_enabled? + end + + def gitlab_auth + if current_user.gitlab_access_token.blank? + go_to_gitlab_for_permissions + end + end + + def go_to_gitlab_for_permissions + redirect_to client.authorize_url(callback_import_gitlab_url) + end + + def gitlab_unauthorized + go_to_gitlab_for_permissions + end +end diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb new file mode 100644 index 00000000000..c121d2de7cb --- /dev/null +++ b/app/controllers/import/gitorious_controller.rb @@ -0,0 +1,43 @@ +class Import::GitoriousController < Import::BaseController + + def new + redirect_to client.authorize_url(callback_import_gitorious_url) + end + + def callback + session[:gitorious_repos] = params[:repos] + redirect_to status_import_gitorious_path + end + + def status + @repos = client.repos + + @already_added_projects = current_user.created_projects.where(import_type: "gitorious") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos.reject! { |repo| already_added_projects_names.include? repo.full_name } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "gitorious").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @repo_id = params[:repo_id] + repo = client.repo(@repo_id) + @target_namespace = params[:new_namespace].presence || repo.namespace + @project_name = repo.name + + namespace = get_or_create_namespace || (render and return) + + @project = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, current_user).execute + end + + private + + def client + @client ||= Gitlab::GitoriousImport::Client.new(session[:gitorious_repos]) + end + +end diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb new file mode 100644 index 00000000000..4aa6d28c9a8 --- /dev/null +++ b/app/controllers/import/google_code_controller.rb @@ -0,0 +1,117 @@ +class Import::GoogleCodeController < Import::BaseController + before_action :user_map, only: [:new_user_map, :create_user_map] + + def new + + end + + def callback + dump_file = params[:dump_file] + + unless dump_file.respond_to?(:read) + return redirect_to :back, alert: "You need to upload a Google Takeout archive." + end + + begin + dump = JSON.parse(dump_file.read) + rescue + return redirect_to :back, alert: "The uploaded file is not a valid Google Takeout archive." + end + + client = Gitlab::GoogleCodeImport::Client.new(dump) + unless client.valid? + return redirect_to :back, alert: "The uploaded file is not a valid Google Takeout archive." + end + + session[:google_code_dump] = dump + + if params[:create_user_map] == "1" + redirect_to new_user_map_import_google_code_path + else + redirect_to status_import_google_code_path + end + end + + def new_user_map + + end + + def create_user_map + user_map_json = params[:user_map] + user_map_json = "{}" if user_map_json.blank? + + begin + user_map = JSON.parse(user_map_json) + rescue + flash.now[:alert] = "The entered user map is not a valid JSON user map." + + render "new_user_map" and return + end + + unless user_map.is_a?(Hash) && user_map.all? { |k, v| k.is_a?(String) && v.is_a?(String) } + flash.now[:alert] = "The entered user map is not a valid JSON user map." + + render "new_user_map" and return + end + + # This is the default, so let's not save it into the database. + user_map.reject! do |key, value| + value == Gitlab::GoogleCodeImport::Client.mask_email(key) + end + + session[:google_code_user_map] = user_map + + flash[:notice] = "The user map has been saved. Continue by selecting the projects you want to import." + + redirect_to status_import_google_code_path + end + + def status + unless client.valid? + return redirect_to new_import_google_code_path + end + + @repos = client.repos + @incompatible_repos = client.incompatible_repos + + @already_added_projects = current_user.created_projects.where(import_type: "google_code") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos.reject! { |repo| already_added_projects_names.include? repo.name } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "google_code").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @repo_id = params[:repo_id] + repo = client.repo(@repo_id) + @target_namespace = current_user.namespace + @project_name = repo.name + + namespace = @target_namespace + + user_map = session[:google_code_user_map] + + @project = Gitlab::GoogleCodeImport::ProjectCreator.new(repo, namespace, current_user, user_map).execute + end + + private + + def client + @client ||= Gitlab::GoogleCodeImport::Client.new(session[:google_code_dump]) + end + + def user_map + @user_map ||= begin + user_map = client.user_map + + stored_user_map = session[:google_code_user_map] + user_map.update(stored_user_map) if stored_user_map + + Hash[user_map.sort] + end + end +end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb new file mode 100644 index 00000000000..eb3c8233530 --- /dev/null +++ b/app/controllers/invites_controller.rb @@ -0,0 +1,81 @@ +class InvitesController < ApplicationController + before_action :member + skip_before_action :authenticate_user!, only: :decline + + respond_to :html + + def show + + end + + def accept + if member.accept_invite!(current_user) + label, path = source_info(member.source) + + redirect_to path, notice: "You have been granted #{member.human_access} access to #{label}." + else + redirect_to :back, alert: "The invitation could not be accepted." + end + end + + def decline + if member.decline_invite! + label, _ = source_info(member.source) + + path = + if current_user + dashboard_path + else + new_user_session_path + end + + redirect_to path, notice: "You have declined the invitation to join #{label}." + else + redirect_to :back, alert: "The invitation could not be declined." + end + end + + private + + def member + return @member if defined?(@member) + + @token = params[:id] + @member = Member.find_by_invite_token(@token) + + unless @member + render_404 and return + end + + @member + end + + def authenticate_user! + return if current_user + + notice = "To accept this invitation, sign in" + notice << " or create an account" if current_application_settings.signup_enabled? + notice << "." + + store_location_for :user, request.fullpath + redirect_to new_user_session_path, notice: notice + end + + def source_info(source) + case source + when Project + project = member.source + label = "project #{project.name_with_namespace}" + path = namespace_project_path(project.namespace, project) + when Group + group = member.source + label = "group #{group.name}" + path = group_path(group) + else + label = "who knows what" + path = dashboard_path + end + + [label, path] + end +end diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb index c59a2401cef..83eec1bf4a2 100644 --- a/app/controllers/namespaces_controller.rb +++ b/app/controllers/namespaces_controller.rb @@ -1,18 +1,25 @@ class NamespacesController < ApplicationController - skip_before_filter :authenticate_user! + skip_before_action :authenticate_user! def show namespace = Namespace.find_by(path: params[:id]) - unless namespace - return render_404 + if namespace + if namespace.is_a?(Group) + group = namespace + else + user = namespace.owner + end end - if namespace.type == "Group" - redirect_to group_path(namespace) + if user + redirect_to user_path(user) + elsif group && can?(current_user, :read_group, group) + redirect_to group_path(group) + elsif current_user.nil? + authenticate_user! else - redirect_to user_path(namespace.owner) + render_404 end end end - diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb new file mode 100644 index 00000000000..fc31118124b --- /dev/null +++ b/app/controllers/oauth/applications_controller.rb @@ -0,0 +1,50 @@ +class Oauth::ApplicationsController < Doorkeeper::ApplicationsController + include Gitlab::CurrentSettings + include PageLayoutHelper + + before_action :verify_user_oauth_applications_enabled + before_action :authenticate_user! + + layout 'profile' + + def index + head :forbidden and return + end + + def create + @application = Doorkeeper::Application.new(application_params) + + @application.owner = current_user + + if @application.save + flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) + redirect_to oauth_application_url(@application) + else + render :new + end + end + + def destroy + if @application.destroy + flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :destroy]) + end + + redirect_to applications_profile_url + end + + private + + def verify_user_oauth_applications_enabled + return if current_application_settings.user_oauth_applications? + + redirect_to applications_profile_url + end + + def set_application + @application = current_user.oauth_applications.find(params[:id]) + end + + rescue_from ActiveRecord::RecordNotFound do |exception| + render "errors/not_found", layout: "errors", status: 404 + end +end diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb new file mode 100644 index 00000000000..24025d8c723 --- /dev/null +++ b/app/controllers/oauth/authorizations_controller.rb @@ -0,0 +1,58 @@ +class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController + before_action :authenticate_resource_owner! + + layout 'profile' + + def new + if pre_auth.authorizable? + if skip_authorization? || matching_token? + auth = authorization.authorize + redirect_to auth.redirect_uri + else + render "doorkeeper/authorizations/new" + end + else + render "doorkeeper/authorizations/error" + end + end + + # TODO: Handle raise invalid authorization + def create + redirect_or_render authorization.authorize + end + + def destroy + redirect_or_render authorization.deny + end + + private + + def matching_token? + Doorkeeper::AccessToken.matching_token_for(pre_auth.client, + current_resource_owner.id, + pre_auth.scopes) + end + + def redirect_or_render(auth) + if auth.redirectable? + redirect_to auth.redirect_uri + else + render json: auth.body, status: auth.status + end + end + + def pre_auth + @pre_auth ||= + Doorkeeper::OAuth::PreAuthorization.new(Doorkeeper.configuration, + server.client_via_uid, + params) + end + + def authorization + @authorization ||= strategy.request + end + + def strategy + @strategy ||= server.authorization_request(pre_auth.response_type) + end +end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb new file mode 100644 index 00000000000..3ab6def511c --- /dev/null +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -0,0 +1,10 @@ +class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController + include PageLayoutHelper + + layout 'profile' + + def destroy + Doorkeeper::AccessToken.revoke_all_for(params[:id], current_resource_owner) + redirect_to applications_profile_url, notice: I18n.t(:notice, scope: [:doorkeeper, :flash, :authorized_applications, :destroy]) + end +end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index bd4b310fcbf..765adaf2128 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -1,4 +1,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController + + protect_from_forgery except: [:kerberos, :saml] + Gitlab.config.omniauth.providers.each do |provider| define_method provider['name'] do handle_omniauth @@ -21,7 +24,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController @user = Gitlab::LDAP::User.new(oauth) @user.save if @user.changed? # will also save new users gl_user = @user.gl_user - gl_user.remember_me = true if @user.persisted? + gl_user.remember_me = params[:remember_me] if @user.persisted? # Do additional LDAP checks for the user filter and EE features if @user.allowed? @@ -42,11 +45,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def handle_omniauth if current_user - # Change a logged-in user's authentication method: - current_user.extern_uid = oauth['uid'] - current_user.provider = oauth['provider'] - current_user.save - redirect_to profile_path + # Add new authentication method + current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider']) + redirect_to profile_account_path, notice: 'Authentication method updated' else @user = Gitlab::OAuth::User.new(oauth) @user.save @@ -67,8 +68,15 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return end end - rescue StandardError - flash[:notice] = "There's no such user!" + rescue Gitlab::OAuth::SignupDisabledError => e + message = "Signing in using your #{oauth['provider']} account without a pre-existing GitLab account is not allowed." + + if current_application_settings.signup_enabled? + message << " Create a GitLab account first, and then connect it to your #{oauth['provider']} account." + end + + flash[:notice] = message + redirect_to new_user_session_path end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 988ede3007b..8450ba31021 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -5,14 +5,55 @@ class PasswordsController < Devise::PasswordsController resource_found = resource_class.find_by_email(email) if resource_found && resource_found.ldap_user? flash[:alert] = "Cannot reset password for LDAP user." - respond_with({}, :location => after_sending_reset_password_instructions_path_for(resource_name)) and return + respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) and return end self.resource = resource_class.send_reset_password_instructions(resource_params) if successfully_sent?(resource) - respond_with({}, :location => after_sending_reset_password_instructions_path_for(resource_name)) + respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) else respond_with(resource) end end + + # After a user resets their password, prompt for 2FA code if enabled instead + # of signing in automatically + # + # See http://git.io/vURrI + def update + super do |resource| + # TODO (rspeicher): In Devise master (> 3.4.1), we can set + # `Devise.sign_in_after_reset_password = false` and avoid this mess. + if resource.errors.empty? && resource.try(:two_factor_enabled?) + resource.unlock_access! if unlockable?(resource) + + # Since we are not signing this user in, we use the :updated_not_active + # message which only contains "Your password was changed successfully." + set_flash_message(:notice, :updated_not_active) if is_flashing_format? + + # Redirect to sign in so they can enter 2FA code + respond_with(resource, location: new_session_path(resource)) and return + end + end + end + + def edit + super + reset_password_token = Devise.token_generator.digest( + User, + :reset_password_token, + resource.reset_password_token + ) + + unless reset_password_token.nil? + user = User.where( + reset_password_token: reset_password_token + ).first_or_initialize + + unless user.reset_password_period_valid? + flash[:alert] = 'Your password reset token has expired.' + redirect_to(new_user_password_url(user_email: user['email'])) + end + end + end end diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index fe121691a10..175afbf8425 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -1,7 +1,11 @@ -class Profiles::AccountsController < ApplicationController - layout "profile" - +class Profiles::AccountsController < Profiles::ApplicationController def show @user = current_user end + + def unlink + provider = params[:provider] + current_user.identities.find_by(provider: provider).destroy + redirect_to profile_account_path + end end diff --git a/app/controllers/profiles/application_controller.rb b/app/controllers/profiles/application_controller.rb new file mode 100644 index 00000000000..c8be288b9a0 --- /dev/null +++ b/app/controllers/profiles/application_controller.rb @@ -0,0 +1,3 @@ +class Profiles::ApplicationController < ApplicationController + layout 'profile' +end diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb index 57f3bbf0627..f193adb46b4 100644 --- a/app/controllers/profiles/avatars_controller.rb +++ b/app/controllers/profiles/avatars_controller.rb @@ -1,6 +1,4 @@ -class Profiles::AvatarsController < ApplicationController - layout "profile" - +class Profiles::AvatarsController < Profiles::ApplicationController def destroy @user = current_user @user.remove_avatar! diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index f3f0e69b83a..0ede9b8e21b 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -1,6 +1,4 @@ -class Profiles::EmailsController < ApplicationController - layout "profile" - +class Profiles::EmailsController < Profiles::ApplicationController def index @primary = current_user.email @emails = current_user.emails @@ -9,7 +7,11 @@ class Profiles::EmailsController < ApplicationController def create @email = current_user.emails.new(email_params) - flash[:alert] = @email.errors.full_messages.first unless @email.save + if @email.save + NotificationService.new.new_email(@email) + else + flash[:alert] = @email.errors.full_messages.first + end redirect_to profile_emails_url end @@ -18,6 +20,8 @@ class Profiles::EmailsController < ApplicationController @email = current_user.emails.find(params[:id]) @email.destroy + current_user.update_secondary_emails! + respond_to do |format| format.html { redirect_to profile_emails_url } format.js { render nothing: true } diff --git a/app/controllers/profiles/groups_controller.rb b/app/controllers/profiles/groups_controller.rb deleted file mode 100644 index ce9dd50df67..00000000000 --- a/app/controllers/profiles/groups_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -class Profiles::GroupsController < ApplicationController - layout "profile" - - def index - @user_groups = current_user.group_members.page(params[:page]).per(20) - end - - def leave - @users_group = group.group_members.where(user_id: current_user.id).first - if can?(current_user, :destroy, @users_group) - @users_group.destroy - redirect_to(profile_groups_path, info: "You left #{group.name} group.") - else - return render_403 - end - end - - private - - def group - @group ||= Group.find_by(path: params[:id]) - end -end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index 88414b13564..f3224148fda 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -1,9 +1,8 @@ -class Profiles::KeysController < ApplicationController - layout "profile" - skip_before_filter :authenticate_user!, only: [:get_keys] +class Profiles::KeysController < Profiles::ApplicationController + skip_before_action :authenticate_user!, only: [:get_keys] def index - @keys = current_user.keys.order('id DESC') + @keys = current_user.keys end def show diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 638d1f9789b..22423651c17 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -1,7 +1,6 @@ -class Profiles::NotificationsController < ApplicationController - layout 'profile' - +class Profiles::NotificationsController < Profiles::ApplicationController def show + @user = current_user @notification = current_user.notification @project_members = current_user.project_members @group_members = current_user.group_members @@ -11,16 +10,33 @@ class Profiles::NotificationsController < ApplicationController type = params[:notification_type] @saved = if type == 'global' - current_user.notification_level = params[:notification_level] - current_user.save + current_user.update_attributes(user_params) elsif type == 'group' - users_group = current_user.group_members.find(params[:notification_id]) - users_group.notification_level = params[:notification_level] - users_group.save + group_member = current_user.group_members.find(params[:notification_id]) + group_member.notification_level = params[:notification_level] + group_member.save else project_member = current_user.project_members.find(params[:notification_id]) project_member.notification_level = params[:notification_level] project_member.save end + + respond_to do |format| + format.html do + if @saved + flash[:notice] = "Notification settings saved" + else + flash[:alert] = "Failed to save new settings" + end + + redirect_to :back + end + + format.js + end + end + + def user_params + params.require(:user).permit(:notification_email, :notification_level) end end diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb index 1191ce47eba..c780e0983f9 100644 --- a/app/controllers/profiles/passwords_controller.rb +++ b/app/controllers/profiles/passwords_controller.rb @@ -1,17 +1,16 @@ -class Profiles::PasswordsController < ApplicationController - layout :determine_layout +class Profiles::PasswordsController < Profiles::ApplicationController + skip_before_action :check_password_expiration, only: [:new, :create] - skip_before_filter :check_password_expiration, only: [:new, :create] + before_action :set_user + before_action :authorize_change_password! - before_filter :set_user - before_filter :set_title - before_filter :authorize_change_password! + layout :determine_layout def new end def create - unless @user.valid_password?(user_params[:current_password]) + unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password]) redirect_to new_profile_password_path, alert: 'You must provide a valid current password' return end @@ -21,7 +20,8 @@ class Profiles::PasswordsController < ApplicationController result = @user.update_attributes( password: new_password, - password_confirmation: new_password_confirmation + password_confirmation: new_password_confirmation, + password_automatically_set: false ) if result @@ -39,8 +39,9 @@ class Profiles::PasswordsController < ApplicationController password_attributes = user_params.select do |key, value| %w(password password_confirmation).include?(key.to_s) end + password_attributes[:password_automatically_set] = false - unless @user.valid_password?(user_params[:current_password]) + unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password]) redirect_to edit_profile_password_path, alert: 'You must provide a valid current password' return end @@ -64,13 +65,9 @@ class Profiles::PasswordsController < ApplicationController @user = current_user end - def set_title - @title = "New password" - end - def determine_layout if [:new, :create].include?(action_name.to_sym) - 'navless' + 'application' else 'profile' end diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb new file mode 100644 index 00000000000..538b09ca54d --- /dev/null +++ b/app/controllers/profiles/preferences_controller.rb @@ -0,0 +1,38 @@ +class Profiles::PreferencesController < Profiles::ApplicationController + before_action :user + + def show + end + + def update + begin + if @user.update_attributes(preferences_params) + flash[:notice] = 'Preferences saved.' + else + flash[:alert] = 'Failed to save preferences.' + end + rescue ArgumentError => e + # Raised when `dashboard` is given an invalid value. + flash[:alert] = "Failed to save preferences (#{e.message})." + end + + respond_to do |format| + format.html { redirect_to profile_preferences_path } + format.js + end + end + + private + + def user + @user = current_user + end + + def preferences_params + params.require(:user).permit( + :color_scheme_id, + :dashboard, + :theme_id + ) + end +end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb new file mode 100644 index 00000000000..03845f1e1ec --- /dev/null +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -0,0 +1,54 @@ +class Profiles::TwoFactorAuthsController < Profiles::ApplicationController + def new + unless current_user.otp_secret + current_user.otp_secret = User.generate_otp_secret(32) + current_user.save! + end + + @qr_code = build_qr_code + end + + def create + if current_user.valid_otp?(params[:pin_code]) + current_user.two_factor_enabled = true + @codes = current_user.generate_otp_backup_codes! + current_user.save! + + render 'create' + else + @error = 'Invalid pin code' + @qr_code = build_qr_code + + render 'new' + end + end + + def codes + @codes = current_user.generate_otp_backup_codes! + current_user.save! + end + + def destroy + current_user.update_attributes({ + two_factor_enabled: false, + encrypted_otp_secret: nil, + encrypted_otp_secret_iv: nil, + encrypted_otp_secret_salt: nil, + otp_backup_codes: nil + }) + + redirect_to profile_account_path + end + + private + + def build_qr_code + issuer = "#{issuer_host} | #{current_user.email}" + uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer) + RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3) + end + + def issuer_host + Gitlab.config.gitlab.host + end +end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index e877f9b9049..b4af9e490ed 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -1,16 +1,17 @@ -class ProfilesController < ApplicationController +class ProfilesController < Profiles::ApplicationController include ActionView::Helpers::SanitizeHelper - before_filter :user - before_filter :authorize_change_username!, only: :update_username - skip_before_filter :require_email, only: [:show, :update] - - layout 'profile' + before_action :user + before_action :authorize_change_username!, only: :update_username + skip_before_action :require_email, only: [:show, :update] def show end - def design + def applications + @applications = current_user.oauth_applications + @authorized_tokens = current_user.oauth_authorized_tokens + @authorized_apps = @authorized_tokens.map(&:application).uniq end def update @@ -19,12 +20,12 @@ class ProfilesController < ApplicationController if @user.update_attributes(user_params) flash[:notice] = "Profile was successfully updated" else - flash[:alert] = "Failed to update profile" + messages = @user.errors.full_messages.uniq.join('. ') + flash[:alert] = "Failed to update profile. #{messages}" end respond_to do |format| format.html { redirect_to :back } - format.js end end @@ -37,7 +38,7 @@ class ProfilesController < ApplicationController end def history - @events = current_user.recent_events.page(params[:page]).per(20) + @events = current_user.recent_events.page(params[:page]).per(PER_PAGE) end def update_username @@ -60,9 +61,21 @@ class ProfilesController < ApplicationController def user_params params.require(:user).permit( - :email, :password, :password_confirmation, :bio, :name, :username, - :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, - :avatar, :hide_no_ssh_key, + :avatar, + :bio, + :email, + :hide_no_password, + :hide_no_ssh_key, + :linkedin, + :location, + :name, + :password, + :password_confirmation, + :public_email, + :skype, + :twitter, + :username, + :website_url ) end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 7e4580017dd..ee88d49b400 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -1,14 +1,15 @@ class Projects::ApplicationController < ApplicationController - before_filter :project - before_filter :repository - layout :determine_layout + before_action :project + before_action :repository + layout 'project' def authenticate_user! # Restrict access to Projects area only # for non-signed users if !current_user id = params[:project_id] || params[:id] - @project = Project.find_with_namespace(id) + project_with_namespace = "#{params[:namespace_id]}/#{id}" + @project = Project.find_with_namespace(project_with_namespace) return if @project && @project.public? end @@ -16,17 +17,12 @@ class Projects::ApplicationController < ApplicationController super end - def determine_layout - if current_user - 'projects' - else - 'public_projects' - end - end - def require_branch_head unless @repository.branch_names.include?(@ref) - redirect_to project_tree_path(@project, @ref), notice: "This action is not allowed unless you are on top of a branch" + redirect_to( + namespace_project_tree_path(@project.namespace, @project, @ref), + notice: "This action is not allowed unless you are on top of a branch" + ) end end end diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb new file mode 100644 index 00000000000..9c3763d5934 --- /dev/null +++ b/app/controllers/projects/avatars_controller.rb @@ -0,0 +1,27 @@ +class Projects::AvatarsController < Projects::ApplicationController + before_action :project + + def show + @blob = @project.repository.blob_at_branch('master', @project.avatar_in_git) + if @blob + headers['X-Content-Type-Options'] = 'nosniff' + send_data( + @blob.data, + type: @blob.mime_type, + disposition: 'inline', + filename: @blob.name + ) + else + not_found! + end + end + + def destroy + @project.remove_avatar! + + @project.save + @project.reset_events_cache + + redirect_to edit_project_path(@project) + end +end diff --git a/app/controllers/projects/base_tree_controller.rb b/app/controllers/projects/base_tree_controller.rb deleted file mode 100644 index a7b1b7b40e8..00000000000 --- a/app/controllers/projects/base_tree_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Projects::BaseTreeController < Projects::ApplicationController - include ExtractsPath - - before_filter :authorize_download_code! - before_filter :require_non_empty_project -end - diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index 367d1295f34..3362264dcce 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -2,12 +2,12 @@ class Projects::BlameController < Projects::ApplicationController include ExtractsPath - # Authorize - before_filter :authorize_download_code! - before_filter :require_non_empty_project + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! def show - @blob = @repository.blob_at(@commit.id, @path) - @blame = Gitlab::Git::Blame.new(project.repository, @commit.id, @path) + @blame = Gitlab::Git::Blame.new(@repository, @commit.id, @path) + @blob = @blame.blob end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 2412800c493..100d3d3b317 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -1,23 +1,71 @@ # Controller for viewing a file's blame class Projects::BlobController < Projects::ApplicationController include ExtractsPath + include ActionView::Helpers::SanitizeHelper - # Authorize - before_filter :authorize_download_code! - before_filter :require_non_empty_project - before_filter :authorize_push_code!, only: [:destroy] + # Raised when given an invalid file path + class InvalidPathError < StandardError; end - before_filter :blob + before_action :require_non_empty_project, except: [:new, :create] + before_action :authorize_download_code! + before_action :authorize_push_code!, only: [:destroy] + before_action :assign_blob_vars + before_action :commit, except: [:new, :create] + before_action :blob, except: [:new, :create] + before_action :from_merge_request, only: [:edit, :update] + before_action :require_branch_head, only: [:edit, :update] + before_action :editor_variables, except: [:show, :preview, :diff] + before_action :after_edit_path, only: [:edit, :update] + + def new + commit unless @repository.empty? + end + + def create + result = Files::CreateService.new(@project, current_user, @commit_params).execute + + if result[:status] == :success + flash[:notice] = "Your changes have been successfully committed" + redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) + else + flash[:alert] = result[:message] + render :new + end + end def show end + def edit + @last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha + end + + def update + result = Files::UpdateService.new(@project, current_user, @commit_params).execute + + if result[:status] == :success + flash[:notice] = "Your changes have been successfully committed" + redirect_to after_edit_path + else + flash[:alert] = result[:message] + render :edit + end + end + + def preview + @content = params[:content] + diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true) + @diff_lines = Gitlab::Diff::Parser.new.parse(diffy.diff.scan(/.*\n/)) + + render layout: false + end + def destroy - result = Files::DeleteService.new(@project, current_user, params, @ref, @path).execute + result = Files::DeleteService.new(@project, current_user, @commit_params).execute if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" - redirect_to project_tree_path(@project, @ref) + redirect_to namespace_project_tree_path(@project.namespace, @project, @target_branch) else flash[:alert] = result[:message] render :show @@ -46,10 +94,70 @@ class Projects::BlobController < Projects::ApplicationController if @blob @blob - elsif tree.entries.any? - redirect_to project_tree_path(@project, File.join(@ref, @path)) and return else + if tree = @repository.tree(@commit.id, @path) + if tree.entries.any? + redirect_to namespace_project_tree_path(@project.namespace, @project, File.join(@ref, @path)) and return + end + end + return not_found! end end + + def commit + @commit = @repository.commit(@ref) + + return not_found! unless @commit + end + + def assign_blob_vars + @id = params[:id] + @ref, @path = extract_ref(@id) + + rescue InvalidPathError + not_found! + end + + def after_edit_path + @after_edit_path ||= + if from_merge_request + diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + + "#file-path-#{hexdigest(@path)}" + elsif @target_branch.present? + namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) + else + namespace_project_blob_path(@project.namespace, @project, @id) + end + end + + def from_merge_request + # If blob edit was initiated from merge request page + @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id]) + end + + def sanitized_new_branch_name + @new_branch ||= sanitize(strip_tags(params[:new_branch])) + end + + def editor_variables + @current_branch = @ref + @target_branch = (sanitized_new_branch_name || @ref) + + @file_path = + if action_name.to_s == 'create' + File.join(@path, File.basename(params[:file_name])) + else + @path + end + + @commit_params = { + file_path: @file_path, + current_branch: @current_branch, + target_branch: @target_branch, + commit_message: params[:commit_message], + file_content: params[:content], + file_content_encoding: params[:encoding] + } + end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index cff1a907dc2..696011b94b9 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -1,15 +1,14 @@ class Projects::BranchesController < Projects::ApplicationController include ActionView::Helpers::SanitizeHelper # Authorize - before_filter :require_non_empty_project - - before_filter :authorize_download_code! - before_filter :authorize_push_code!, only: [:create, :destroy] + before_action :require_non_empty_project + before_action :authorize_download_code! + before_action :authorize_push_code!, only: [:create, :destroy] def index @sort = params[:sort] || 'name' @branches = @repository.branches_sorted_by(@sort) - @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(30) + @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE) end def recent @@ -24,7 +23,8 @@ class Projects::BranchesController < Projects::ApplicationController if result[:status] == :success @branch = result[:branch] - redirect_to project_tree_path(@project, @branch.name) + redirect_to namespace_project_tree_path(@project.namespace, @project, + @branch.name) else @error = result[:message] render action: 'new' @@ -36,7 +36,10 @@ class Projects::BranchesController < Projects::ApplicationController @branch_name = params[:id] respond_to do |format| - format.html { redirect_to project_branches_path(@project) } + format.html do + redirect_to namespace_project_branches_path(@project.namespace, + @project) + end format.js end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index dac858d8e16..78d42d695b6 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -3,19 +3,18 @@ # Not to be confused with CommitsController, plural. class Projects::CommitController < Projects::ApplicationController # Authorize - before_filter :authorize_download_code! - before_filter :require_non_empty_project - before_filter :commit + before_action :require_non_empty_project + before_action :authorize_download_code! + before_action :commit def show return git_not_found! unless @commit - @line_notes = @project.notes.for_commit_id(commit.id).inline - @branches = @project.repository.branch_names_contains(commit.id) + @line_notes = commit.notes.inline @diffs = @commit.diffs @note = @project.build_commit_note(commit) - @notes_count = @project.notes.for_commit_id(commit.id).count - @notes = @project.notes.for_commit_id(@commit.id).not_inline.fresh + @notes_count = commit.notes.count + @notes = commit.notes.not_inline.fresh @noteable = @commit @comments_allowed = @reply_allowed = true @comments_target = { @@ -30,7 +29,13 @@ class Projects::CommitController < Projects::ApplicationController end end + def branches + @branches = @project.repository.branch_names_contains(commit.id) + @tags = @project.repository.tag_names_contains(commit.id) + render layout: false + end + def commit - @commit ||= @project.repository.commit(params[:id]) + @commit ||= @project.commit(params[:id]) end end diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 9476b6c0284..d1c15174aea 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -3,9 +3,9 @@ require "base64" class Projects::CommitsController < Projects::ApplicationController include ExtractsPath - # Authorize - before_filter :authorize_download_code! - before_filter :require_non_empty_project + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! def show @repo = @project.repository @@ -13,7 +13,7 @@ class Projects::CommitsController < Projects::ApplicationController @commits = @repo.commits(@ref, @path, @limit, @offset) @note_counts = Note.where(commit_id: @commits.map(&:id)). - group(:commit_id).count + group(:commit_id).count respond_to do |format| format.html diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index ffb8c2e4af1..c5f085c236f 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -1,14 +1,17 @@ +require 'addressable/uri' + class Projects::CompareController < Projects::ApplicationController # Authorize - before_filter :authorize_download_code! - before_filter :require_non_empty_project + before_action :require_non_empty_project + before_action :authorize_download_code! def index + @ref = Addressable::URI.unescape(params[:to]) end def show - base_ref = params[:from] - head_ref = params[:to] + base_ref = Addressable::URI.unescape(params[:from]) + @ref = head_ref = Addressable::URI.unescape(params[:to]) compare_result = CompareService.new.execute( current_user, @@ -25,6 +28,7 @@ class Projects::CompareController < Projects::ApplicationController end def create - redirect_to project_compare_path(@project, params[:from], params[:to]) + redirect_to namespace_project_compare_path(@project.namespace, @project, + params[:from], params[:to]) end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 024b9520d30..40e2b37912b 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -2,17 +2,20 @@ class Projects::DeployKeysController < Projects::ApplicationController respond_to :html # Authorize - before_filter :authorize_admin_project! + before_action :authorize_admin_project! layout "project_settings" def index @enabled_keys = @project.deploy_keys - @available_keys = available_keys - @enabled_keys - end - def show - @key = @project.deploy_keys.find(params[:id]) + @available_keys = accessible_keys - @enabled_keys + @available_project_keys = current_user.project_deploy_keys - @enabled_keys + @available_public_keys = DeployKey.are_public - @enabled_keys + + # Public keys that are already used by another accessible project are already + # in @available_project_keys. + @available_public_keys -= @available_project_keys end def new @@ -25,38 +28,31 @@ class Projects::DeployKeysController < Projects::ApplicationController @key = DeployKey.new(deploy_key_params) if @key.valid? && @project.deploy_keys << @key - redirect_to project_deploy_keys_path(@project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, + @project) else render "new" end end - def destroy - @key = @project.deploy_keys.find(params[:id]) - @key.destroy - - respond_to do |format| - format.html { redirect_to project_deploy_keys_url } - format.js { render nothing: true } - end - end - def enable - @project.deploy_keys << available_keys.find(params[:id]) + @key = accessible_keys.find(params[:id]) + @project.deploy_keys << @key - redirect_to project_deploy_keys_path(@project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, + @project) end def disable - @project.deploy_keys_projects.where(deploy_key_id: params[:id]).last.destroy + @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy - redirect_to project_deploy_keys_path(@project) + redirect_to :back end protected - def available_keys - @available_keys ||= current_user.accessible_deploy_keys + def accessible_keys + @accessible_keys ||= current_user.accessible_deploy_keys end def deploy_key_params diff --git a/app/controllers/projects/edit_tree_controller.rb b/app/controllers/projects/edit_tree_controller.rb deleted file mode 100644 index 65661c80410..00000000000 --- a/app/controllers/projects/edit_tree_controller.rb +++ /dev/null @@ -1,60 +0,0 @@ -class Projects::EditTreeController < Projects::BaseTreeController - before_filter :require_branch_head - before_filter :blob - before_filter :authorize_push_code! - before_filter :from_merge_request - before_filter :after_edit_path - - def show - @last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha - end - - def update - result = Files::UpdateService. - new(@project, current_user, params, @ref, @path).execute - - if result[:status] == :success - flash[:notice] = "Your changes have been successfully committed" - - if from_merge_request - from_merge_request.reload_code - end - - redirect_to after_edit_path - else - flash[:alert] = result[:message] - render :show - end - end - - def preview - @content = params[:content] - - diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', - include_diff_info: true) - @diff_lines = Gitlab::Diff::Parser.new.parse(diffy.diff.scan(/.*\n/)) - - render layout: false - end - - private - - def blob - @blob ||= @repository.blob_at(@commit.id, @path) - end - - def after_edit_path - @after_edit_path ||= - if from_merge_request - diffs_project_merge_request_path(from_merge_request.target_project, from_merge_request) + - "#file-path-#{hexdigest(@path)}" - else - project_blob_path(@project, @id) - end - end - - def from_merge_request - # If blob edit was initiated from merge request page - @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id]) - end -end diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index a0481d11582..9e72597ea87 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -1,7 +1,7 @@ class Projects::ForksController < Projects::ApplicationController # Authorize - before_filter :authorize_download_code! - before_filter :require_non_empty_project + before_action :require_non_empty_project + before_action :authorize_download_code! def new @namespaces = current_user.manageable_namespaces @@ -9,13 +9,15 @@ class Projects::ForksController < Projects::ApplicationController end def create - namespace = Namespace.find(params[:namespace_id]) + namespace = Namespace.find(params[:namespace_key]) @forked_project = ::Projects::ForkService.new(project, current_user, namespace: namespace).execute if @forked_project.saved? && @forked_project.forked? - redirect_to(@forked_project, notice: 'Project was successfully forked.') + redirect_to( + namespace_project_path(@forked_project.namespace, @forked_project), + notice: 'Project was successfully forked.' + ) else - @title = 'Fork project' render :error end end diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 4a318cb7d56..a060ea6f998 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -1,7 +1,7 @@ class Projects::GraphsController < Projects::ApplicationController # Authorize - before_filter :authorize_download_code! - before_filter :require_non_empty_project + before_action :require_non_empty_project + before_action :authorize_download_code! def show respond_to do |format| @@ -28,8 +28,8 @@ class Projects::GraphsController < Projects::ApplicationController @commits.each do |commit| @log << { - author_name: commit.author_name.force_encoding('UTF-8'), - author_email: commit.author_email.force_encoding('UTF-8'), + author_name: commit.author_name, + author_email: commit.author_email, date: commit.committed_date.strftime("%Y-%m-%d") } end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index cab8fd76e6c..76062446c92 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -1,6 +1,6 @@ class Projects::HooksController < Projects::ApplicationController # Authorize - before_filter :authorize_admin_project! + before_action :authorize_admin_project! respond_to :html @@ -16,7 +16,7 @@ class Projects::HooksController < Projects::ApplicationController @hook.save if @hook.valid? - redirect_to project_hooks_path(@project) + redirect_to namespace_project_hooks_path(@project.namespace, @project) else @hooks = @project.hooks.select(&:persisted?) render :index @@ -26,6 +26,7 @@ class Projects::HooksController < Projects::ApplicationController def test if !@project.empty_repo? status = TestHookService.new.execute(hook, current_user) + if status flash[:notice] = 'Hook successfully executed.' else @@ -42,7 +43,7 @@ class Projects::HooksController < Projects::ApplicationController def destroy hook.destroy - redirect_to project_hooks_path(@project) + redirect_to namespace_project_hooks_path(@project.namespace, @project) end private @@ -52,6 +53,6 @@ class Projects::HooksController < Projects::ApplicationController end def hook_params - params.require(:hook).permit(:url, :push_events, :issues_events, :merge_requests_events, :tag_push_events) + params.require(:hook).permit(:url, :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events) end end diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index b8350642804..066b66014f8 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -1,8 +1,8 @@ class Projects::ImportsController < Projects::ApplicationController # Authorize - before_filter :authorize_admin_project! - before_filter :require_no_repo - before_filter :redirect_if_progress, except: :show + before_action :authorize_admin_project! + before_action :require_no_repo + before_action :redirect_if_progress, except: :show def new end @@ -20,15 +20,16 @@ class Projects::ImportsController < Projects::ApplicationController end end - redirect_to project_import_path(@project) + redirect_to namespace_project_import_path(@project.namespace, @project) end def show unless @project.import_in_progress? if @project.import_finished? - redirect_to(@project) and return + redirect_to(project_path(@project)) and return else - redirect_to new_project_import_path(@project) and return + redirect_to new_namespace_project_import_path(@project.namespace, + @project) && return end end end @@ -36,14 +37,15 @@ class Projects::ImportsController < Projects::ApplicationController private def require_no_repo - if @project.repository_exists? - redirect_to(@project) and return + if @project.repository_exists? && !@project.import_in_progress? + redirect_to(namespace_project_path(@project.namespace, @project)) and return end end def redirect_if_progress if @project.import_in_progress? - redirect_to project_import_path(@project) and return + redirect_to namespace_project_import_path(@project.namespace, @project) && + return end end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index c6d526f05c5..7d168aa827b 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -1,34 +1,34 @@ class Projects::IssuesController < Projects::ApplicationController - before_filter :module_enabled - before_filter :issue, only: [:edit, :update, :show] + before_action :module_enabled + before_action :issue, only: [:edit, :update, :show, :toggle_subscription] # Allow read any issue - before_filter :authorize_read_issue! + before_action :authorize_read_issue! # Allow write(create) issue - before_filter :authorize_write_issue!, only: [:new, :create] + before_action :authorize_write_issue!, only: [:new, :create] # Allow modify issue - before_filter :authorize_modify_issue!, only: [:edit, :update] + before_action :authorize_modify_issue!, only: [:edit, :update] # Allow issues bulk update - before_filter :authorize_admin_issues!, only: [:bulk_update] + before_action :authorize_admin_issues!, only: [:bulk_update] respond_to :html def index terms = params['issue_search'] + @issues = get_issues_collection - @issues = issues_filtered - @issues = @issues.full_search(terms) if terms.present? - @issues = @issues.page(params[:page]).per(20) + if terms.present? + if terms =~ /\A#(\d+)\z/ + @issues = @issues.where(iid: $1) + else + @issues = @issues.full_search(terms) + end + end - assignee_id, milestone_id = params[:assignee_id], params[:milestone_id] - @assignee = @project.team.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero? - @milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero? - sort_param = params[:sort] || 'newest' - @sort = sort_param.humanize unless sort_param.empty? - @assignees = User.where(id: @project.issues.pluck(:assignee_id)).active + @issues = @issues.page(params[:page]).per(PER_PAGE) respond_to do |format| format.html @@ -68,7 +68,7 @@ class Projects::IssuesController < Projects::ApplicationController respond_to do |format| format.html do if @issue.valid? - redirect_to project_issue_path(@project, @issue) + redirect_to issue_path(@issue) else render :new end @@ -86,7 +86,7 @@ class Projects::IssuesController < Projects::ApplicationController format.js format.html do if @issue.valid? - redirect_to [@project, @issue] + redirect_to issue_path(@issue) else render :edit end @@ -101,10 +101,16 @@ class Projects::IssuesController < Projects::ApplicationController end def bulk_update - result = Issues::BulkUpdateService.new(project, current_user, params).execute + result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute redirect_to :back, notice: "#{result[:count]} issues updated" end + def toggle_subscription + @issue.toggle_subscription(current_user) + + render nothing: true + end + protected def issue @@ -127,12 +133,6 @@ class Projects::IssuesController < Projects::ApplicationController return render_404 unless @project.issues_enabled end - def issues_filtered - params[:scope] = 'all' if params[:scope].blank? - params[:state] = 'opened' if params[:state].blank? - @issues = IssuesFinder.new.execute(current_user, params.merge(project_id: @project.id)) - end - # Since iids are implemented only in 6.1 # user may navigate to issue page using old global ids. # @@ -142,7 +142,7 @@ class Projects::IssuesController < Projects::ApplicationController issue = @project.issues.find_by(id: params[:id]) if issue - redirect_to project_issue_path(@project, issue) + redirect_to issue_path(issue) return else raise ActiveRecord::RecordNotFound.new @@ -155,4 +155,13 @@ class Projects::IssuesController < Projects::ApplicationController :milestone_id, :state_event, :task_num, label_ids: [] ) end + + def bulk_update_params + params.require(:update).permit( + :issues_ids, + :assignee_id, + :milestone_id, + :state_event + ) + end end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 6c7bde9c5d5..86d6e3e0f6b 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -1,13 +1,13 @@ class Projects::LabelsController < Projects::ApplicationController - before_filter :module_enabled - before_filter :label, only: [:edit, :update, :destroy] - before_filter :authorize_labels! - before_filter :authorize_admin_labels!, except: [:index] + before_action :module_enabled + before_action :label, only: [:edit, :update, :destroy] + before_action :authorize_read_label! + before_action :authorize_admin_labels!, except: [:index] respond_to :js, :html def index - @labels = @project.labels.order_by_name.page(params[:page]).per(20) + @labels = @project.labels.page(params[:page]).per(PER_PAGE) end def new @@ -18,7 +18,7 @@ class Projects::LabelsController < Projects::ApplicationController @label = @project.labels.create(label_params) if @label.valid? - redirect_to project_labels_path(@project) + redirect_to namespace_project_labels_path(@project.namespace, @project) else render 'new' end @@ -29,7 +29,7 @@ class Projects::LabelsController < Projects::ApplicationController def update if @label.update_attributes(label_params) - redirect_to project_labels_path(@project) + redirect_to namespace_project_labels_path(@project.namespace, @project) else render 'edit' end @@ -39,11 +39,12 @@ class Projects::LabelsController < Projects::ApplicationController Gitlab::IssuesLabels.generate(@project) if params[:redirect] == 'issues' - redirect_to project_issues_path(@project) + redirect_to namespace_project_issues_path(@project.namespace, @project) elsif params[:redirect] == 'merge_requests' - redirect_to project_merge_requests_path(@project) + redirect_to namespace_project_merge_requests_path(@project.namespace, + @project) else - redirect_to project_labels_path(@project) + redirect_to namespace_project_labels_path(@project.namespace, @project) end end @@ -51,7 +52,10 @@ class Projects::LabelsController < Projects::ApplicationController @label.destroy respond_to do |format| - format.html { redirect_to project_labels_path(@project), notice: 'Label was removed' } + format.html do + redirect_to(namespace_project_labels_path(@project.namespace, @project), + notice: 'Label was removed') + end format.js end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 20a733b10e1..51ecbfd561a 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -1,42 +1,55 @@ require 'gitlab/satellite/satellite' class Projects::MergeRequestsController < Projects::ApplicationController - before_filter :module_enabled - before_filter :merge_request, only: [:edit, :update, :show, :diffs, :automerge, :automerge_check, :ci_status] - before_filter :closes_issues, only: [:edit, :update, :show, :diffs] - before_filter :validates_merge_request, only: [:show, :diffs] - before_filter :define_show_vars, only: [:show, :diffs] + before_action :module_enabled + before_action :merge_request, only: [ + :edit, :update, :show, :diffs, :commits, :automerge, :automerge_check, + :ci_status, :toggle_subscription + ] + before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits] + before_action :validates_merge_request, only: [:show, :diffs, :commits] + before_action :define_show_vars, only: [:show, :diffs, :commits] # Allow read any merge_request - before_filter :authorize_read_merge_request! + before_action :authorize_read_merge_request! # Allow write(create) merge_request - before_filter :authorize_write_merge_request!, only: [:new, :create] + before_action :authorize_write_merge_request!, only: [:new, :create] # Allow modify merge_request - before_filter :authorize_modify_merge_request!, only: [:close, :edit, :update, :sort] + before_action :authorize_modify_merge_request!, only: [:close, :edit, :update, :sort] def index - params[:sort] ||= 'newest' - params[:scope] = 'all' if params[:scope].blank? - params[:state] = 'opened' if params[:state].blank? - - @merge_requests = MergeRequestsFinder.new.execute(current_user, params.merge(project_id: @project.id)) - @merge_requests = @merge_requests.page(params[:page]).per(20) - - @sort = params[:sort].humanize - assignee_id, milestone_id = params[:assignee_id], params[:milestone_id] - @assignee = @project.team.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero? - @milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero? - @assignees = User.where(id: @project.merge_requests.pluck(:assignee_id)) + terms = params['issue_search'] + @merge_requests = get_merge_requests_collection + + if terms.present? + if terms =~ /\A[#!](\d+)\z/ + @merge_requests = @merge_requests.where(iid: $1) + else + @merge_requests = @merge_requests.full_search(terms) + end + end + + @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("projects/merge_requests/_merge_requests") + } + end + end end def show @note_counts = Note.where(commit_id: @merge_request.commits.map(&:id)). - group(:commit_id).count + group(:commit_id).count respond_to do |format| format.html + format.json { render json: @merge_request } format.diff { render text: @merge_request.to_diff(current_user) } format.patch { render text: @merge_request.to_patch(current_user) } end @@ -57,6 +70,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def commits + respond_to do |format| + format.html { render 'show' } + format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_commits') } } + end + end + def new params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute @@ -87,7 +107,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute if @merge_request.valid? - redirect_to project_merge_request_path(@merge_request.target_project, @merge_request), notice: 'Merge request was successfully created.' + redirect_to(merge_request_path(@merge_request)) else @source_project = @merge_request.source_project @target_project = @merge_request.target_project @@ -102,7 +122,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.js format.html do - redirect_to [@merge_request.target_project, @merge_request], notice: 'Merge request was successfully updated.' + redirect_to([@merge_request.target_project.namespace.becomes(Namespace), + @merge_request.target_project, @merge_request]) + end + format.json do + render json: { + saved: @merge_request.valid?, + assignee_avatar_url: @merge_request.assignee.try(:avatar_url) + } end end else @@ -114,15 +141,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController if @merge_request.unchecked? @merge_request.check_if_can_be_merged end - render json: {merge_status: @merge_request.merge_status_name} + + closes_issues + + render partial: "projects/merge_requests/widget/show.html.haml", layout: false end def automerge - return access_denied! unless allowed_to_merge? + return access_denied! unless @merge_request.can_be_merged_by?(current_user) - if @merge_request.open? && @merge_request.can_be_merged? - @merge_request.should_remove_source_branch = params[:should_remove_source_branch] - @merge_request.automerge!(current_user, params[:commit_message]) + if @merge_request.automergeable? + AutoMergeWorker.perform_async(@merge_request.id, current_user.id, params) @status = true else @status = false @@ -137,7 +166,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def branch_to @target_project = selected_target_project - @commit = @target_project.repository.commit(params[:ref]) if params[:ref].present? + @commit = @target_project.commit(params[:ref]) if params[:ref].present? end def update_branches @@ -151,10 +180,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController def ci_status ci_service = @merge_request.source_project.ci_service - status = ci_service.commit_status(merge_request.last_commit.sha) + status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.last_commit.sha) + coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) end response = { @@ -165,6 +194,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController render json: response end + def toggle_subscription + @merge_request.toggle_subscription(current_user) + + render nothing: true + end + protected def selected_target_project @@ -222,13 +257,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commits = @merge_request.commits @merge_request_diff = @merge_request.merge_request_diff - @allowed_to_merge = allowed_to_merge? - @show_merge_controls = @merge_request.open? && @commits.any? && @allowed_to_merge @source_branch = @merge_request.source_project.repository.find_branch(@merge_request.source_branch).try(:name) - end - def allowed_to_merge? - allowed_to_push_code?(project, @merge_request.target_branch) + if @merge_request.locked_long_ago? + @merge_request.unlock_mr + @merge_request.close + end end def invalid_mr @@ -236,16 +270,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController render 'invalid' end - def allowed_to_push_code?(project, branch) - action = if project.protected_branch?(branch) - :push_code_to_protected_branches - else - :push_code - end - - can?(current_user, action, project) - end - def merge_request_params params.require(:merge_request).permit( :title, :assignee_id, :source_project_id, :source_branch, diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index f362f449e70..61689488d13 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -1,24 +1,24 @@ class Projects::MilestonesController < Projects::ApplicationController - before_filter :module_enabled - before_filter :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests] + before_action :module_enabled + before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests] # Allow read any milestone - before_filter :authorize_read_milestone! + before_action :authorize_read_milestone! # Allow admin milestone - before_filter :authorize_admin_milestone!, except: [:index, :show] + before_action :authorize_admin_milestone!, except: [:index, :show] respond_to :html def index - @milestones = case params[:f] + @milestones = case params[:state] when 'all'; @project.milestones.order("state, due_date DESC") when 'closed'; @project.milestones.closed.order("due_date DESC") else @project.milestones.active.order("due_date ASC") end @milestones = @milestones.includes(:project) - @milestones = @milestones.page(params[:page]).per(20) + @milestones = @milestones.page(params[:page]).per(PER_PAGE) end def new @@ -40,7 +40,8 @@ class Projects::MilestonesController < Projects::ApplicationController @milestone = Milestones::CreateService.new(project, current_user, milestone_params).execute if @milestone.save - redirect_to project_milestone_path(@project, @milestone) + redirect_to namespace_project_milestone_path(@project.namespace, + @project, @milestone) else render "new" end @@ -53,7 +54,8 @@ class Projects::MilestonesController < Projects::ApplicationController format.js format.html do if @milestone.valid? - redirect_to [@project, @milestone] + redirect_to namespace_project_milestone_path(@project.namespace, + @project, @milestone) else render :edit end @@ -67,7 +69,7 @@ class Projects::MilestonesController < Projects::ApplicationController @milestone.destroy respond_to do |format| - format.html { redirect_to project_milestones_path } + format.html { redirect_to namespace_project_milestones_path } format.js { render nothing: true } end end diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index ada1aed0df7..06aef91cadd 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -2,9 +2,9 @@ class Projects::NetworkController < Projects::ApplicationController include ExtractsPath include ApplicationHelper - # Authorize - before_filter :authorize_download_code! - before_filter :require_non_empty_project + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! def show respond_to do |format| diff --git a/app/controllers/projects/new_tree_controller.rb b/app/controllers/projects/new_tree_controller.rb deleted file mode 100644 index ffba706b2f6..00000000000 --- a/app/controllers/projects/new_tree_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -class Projects::NewTreeController < Projects::BaseTreeController - before_filter :require_branch_head - before_filter :authorize_push_code! - - def show - end - - def update - file_path = File.join(@path, File.basename(params[:file_name])) - result = Files::CreateService.new(@project, current_user, params, @ref, file_path).execute - - if result[:status] == :success - flash[:notice] = "Your changes have been successfully committed" - redirect_to project_blob_path(@project, File.join(@ref, file_path)) - else - flash[:alert] = result[:message] - render :show - end - end -end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 7b08b79d236..f3e521adb69 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -1,12 +1,12 @@ class Projects::NotesController < Projects::ApplicationController # Authorize - before_filter :authorize_read_note! - before_filter :authorize_write_note!, only: [:create] - before_filter :authorize_admin_note!, only: [:update, :destroy] + before_action :authorize_read_note! + before_action :authorize_write_note!, only: [:create] + before_action :authorize_admin_note!, only: [:update, :destroy] + before_action :find_current_user_notes, except: [:destroy, :delete_attachment] def index current_fetched_at = Time.now.to_i - @notes = NotesFinder.new.execute(project, current_user, params) notes_json = { notes: [], last_fetched_at: current_fetched_at } @@ -61,10 +61,6 @@ class Projects::NotesController < Projects::ApplicationController end end - def preview - render text: view_context.markdown(params[:note]) - end - private def note @@ -81,11 +77,24 @@ class Projects::NotesController < Projects::ApplicationController end def note_to_discussion_html(note) + if params[:view] == 'parallel' + template = "projects/notes/_diff_notes_with_reply_parallel" + locals = + if params[:line_type] == 'old' + { notes_left: [note], notes_right: [] } + else + { notes_left: [], notes_right: [note] } + end + else + template = "projects/notes/_diff_notes_with_reply" + locals = { notes: [note] } + end + render_to_string( - "projects/notes/_diff_notes_with_reply", + template, layout: false, formats: [:html], - locals: { notes: [note] } + locals: locals ) end @@ -120,4 +129,10 @@ class Projects::NotesController < Projects::ApplicationController :attachment, :line_code, :commit_id ) end + + private + + def find_current_user_notes + @notes = NotesFinder.new.execute(project, current_user, params) + end end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb new file mode 100644 index 00000000000..b82b6f45d59 --- /dev/null +++ b/app/controllers/projects/project_members_controller.rb @@ -0,0 +1,100 @@ +class Projects::ProjectMembersController < Projects::ApplicationController + # Authorize + before_action :authorize_admin_project!, except: :leave + + def index + @project_members = @project.project_members + @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) + + if params[:search].present? + users = @project.users.search(params[:search]).to_a + @project_members = @project_members.where(user_id: users) + end + + @project_members = @project_members.order('access_level DESC') + + @group = @project.group + if @group + @group_members = @group.group_members + @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group) + + if params[:search].present? + users = @group.users.search(params[:search]).to_a + @group_members = @group_members.where(user_id: users) + end + + @group_members = @group_members.order('access_level DESC').limit(20) + end + + @project_member = @project.project_members.new + end + + def new + @project_member = @project.project_members.new + end + + def create + @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user) + + redirect_to namespace_project_project_members_path(@project.namespace, @project) + end + + def update + @project_member = @project.project_members.find(params[:id]) + @project_member.update_attributes(member_params) + end + + def destroy + @project_member = @project.project_members.find(params[:id]) + @project_member.destroy + + respond_to do |format| + format.html do + redirect_to namespace_project_project_members_path(@project.namespace, @project) + end + format.js { render nothing: true } + end + end + + def resend_invite + redirect_path = namespace_project_project_members_path(@project.namespace, @project) + + @project_member = @project.project_members.find(params[:id]) + + if @project_member.invite? + @project_member.resend_invite + + redirect_to redirect_path, notice: 'The invitation was successfully resent.' + else + redirect_to redirect_path, alert: 'The invitation has already been accepted.' + end + end + + def leave + if @project.namespace == current_user.namespace + return redirect_to(:back, alert: 'You can not leave your own project. Transfer or delete the project.') + end + + @project.project_members.find_by(user_id: current_user).destroy + + respond_to do |format| + format.html { redirect_to dashboard_path } + format.js { render nothing: true } + end + end + + def apply_import + giver = Project.find(params[:source_project_id]) + status = @project.team.import(giver, current_user) + notice = status ? "Successfully imported" : "Import failed" + + redirect_to(namespace_project_project_members_path(project.namespace, project), + notice: notice) + end + + protected + + def member_params + params.require(:project_member).permit(:user_id, :access_level) + end +end diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index bd31b1d3c54..6b52eccebf7 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -1,7 +1,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController # Authorize - before_filter :require_non_empty_project - before_filter :authorize_admin_project! + before_action :require_non_empty_project + before_action :authorize_admin_project! layout "project_settings" @@ -12,14 +12,33 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController def create @project.protected_branches.create(protected_branch_params) - redirect_to project_protected_branches_path(@project) + redirect_to namespace_project_protected_branches_path(@project.namespace, + @project) + end + + def update + protected_branch = @project.protected_branches.find(params[:id]) + + if protected_branch && + protected_branch.update_attributes( + developers_can_push: params[:developers_can_push] + ) + + respond_to do |format| + format.json { render json: protected_branch, status: :ok } + end + else + respond_to do |format| + format.json { render json: protected_branch.errors, status: :unprocessable_entity } + end + end end def destroy @project.protected_branches.find(params[:id]).destroy respond_to do |format| - format.html { redirect_to project_protected_branches_path } + format.html { redirect_to namespace_project_protected_branches_path } format.js { render nothing: true } end end @@ -27,6 +46,6 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController private def protected_branch_params - params.require(:protected_branch).permit(:name) + params.require(:protected_branch).permit(:name, :developers_can_push) end end diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index fdbc4c5a098..647c1454078 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -2,9 +2,9 @@ class Projects::RawController < Projects::ApplicationController include ExtractsPath - # Authorize - before_filter :authorize_download_code! - before_filter :require_non_empty_project + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! def show @blob = @repository.blob_at(@commit.id, @path) @@ -35,4 +35,3 @@ class Projects::RawController < Projects::ApplicationController end end end - diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 5d9336bdc49..01ca1537c0e 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -1,21 +1,23 @@ class Projects::RefsController < Projects::ApplicationController include ExtractsPath - # Authorize - before_filter :authorize_download_code! - before_filter :require_non_empty_project + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! def switch respond_to do |format| format.html do new_path = if params[:destination] == "tree" - project_tree_path(@project, (@id)) + namespace_project_tree_path(@project.namespace, @project, + (@id)) elsif params[:destination] == "blob" - project_blob_path(@project, (@id)) + namespace_project_blob_path(@project.namespace, @project, + (@id)) elsif params[:destination] == "graph" - project_network_path(@project, @id, @options) + namespace_project_network_path(@project.namespace, @project, @id, @options) else - project_commits_path(@project, @id) + namespace_project_commits_path(@project.namespace, @project, @id) end redirect_to new_path @@ -31,19 +33,19 @@ class Projects::RefsController < Projects::ApplicationController def logs_tree @offset = if params[:offset].present? - params[:offset].to_i - else - 0 - end + params[:offset].to_i + else + 0 + end @limit = 25 @path = params[:path] contents = [] - contents += tree.trees - contents += tree.blobs - contents += tree.submodules + contents.push(*tree.trees) + contents.push(*tree.blobs) + contents.push(*tree.submodules) @logs = contents[@offset, @limit].to_a.map do |content| file = @path ? File.join(@path, content.name) : content.name @@ -53,5 +55,10 @@ class Projects::RefsController < Projects::ApplicationController commit: last_commit } end + + respond_to do |format| + format.html { render_404 } + format.js + end end end diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 3a90c1c806d..c4a5e2d6359 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -1,28 +1,28 @@ class Projects::RepositoriesController < Projects::ApplicationController # Authorize - before_filter :authorize_download_code! - before_filter :require_non_empty_project, except: :create - before_filter :authorize_admin_project!, only: :create + before_action :require_non_empty_project, except: :create + before_action :authorize_download_code! + before_action :authorize_admin_project!, only: :create def create @project.create_repository - redirect_to @project + redirect_to project_path(@project) end def archive - unless can?(current_user, :download_code, @project) - render_404 and return + begin + file_path = ArchiveRepositoryService.new(@project, params[:ref], params[:format]).execute + rescue + return head :not_found end - file_path = ArchiveRepositoryService.new.execute(@project, params[:ref], params[:format]) - if file_path # Send file to user response.headers["Content-Length"] = File.open(file_path).size.to_s send_file file_path else - render_404 + redirect_to request.fullpath end end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index c50a1f1e75b..dc18bbd8d5b 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -1,7 +1,16 @@ class Projects::ServicesController < Projects::ApplicationController + ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_version, :subdomain, + :room, :recipients, :project_url, :webhook, + :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, + :build_key, :server, :teamcity_url, :build_type, + :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, + :colorize_messages, :channels, + :push_events, :issues_events, :merge_requests_events, :tag_push_events, + :note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url, + :notify, :color] # Authorize - before_filter :authorize_admin_project! - before_filter :service, only: [:edit, :update, :test] + before_action :authorize_admin_project! + before_action :service, only: [:edit, :update, :test] respond_to :html @@ -9,7 +18,7 @@ class Projects::ServicesController < Projects::ApplicationController def index @project.build_missing_services - @services = @project.services.reload + @services = @project.services.visible.reload end def edit @@ -17,18 +26,25 @@ class Projects::ServicesController < Projects::ApplicationController def update if @service.update_attributes(service_params) - redirect_to edit_project_service_path(@project, @service.to_param) + redirect_to( + edit_namespace_project_service_path(@project.namespace, @project, + @service.to_param, notice: + 'Successfully updated.') + ) else render 'edit' end end def test - data = GitPushService.new.sample_data(project, current_user) - - @service.execute(data) + data = Gitlab::PushDataBuilder.build_sample(project, current_user) + if @service.execute(data) + message = { notice: 'We sent a request to the provided URL' } + else + message = { alert: 'We tried to send a request to the provided URL but an error occured' } + end - redirect_to :back + redirect_to :back, message end private @@ -38,11 +54,6 @@ class Projects::ServicesController < Projects::ApplicationController end def service_params - params.require(:service).permit( - :title, :token, :type, :active, :api_key, :subdomain, - :room, :recipients, :project_url, :webhook, - :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, - :build_key, :server - ) + params.require(:service).permit(ALLOWED_PARAMS) end end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 9d5dd8a95cc..3d75abcc29d 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,18 +1,18 @@ class Projects::SnippetsController < Projects::ApplicationController - before_filter :module_enabled - before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw] + before_action :module_enabled + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read any snippet - before_filter :authorize_read_project_snippet! + before_action :authorize_read_project_snippet! # Allow write(create) snippet - before_filter :authorize_write_project_snippet!, only: [:new, :create] + before_action :authorize_write_project_snippet!, only: [:new, :create] # Allow modify snippet - before_filter :authorize_modify_project_snippet!, only: [:edit, :update] + before_action :authorize_modify_project_snippet!, only: [:edit, :update] # Allow destroy snippet - before_filter :authorize_admin_project_snippet!, only: [:destroy] + before_action :authorize_admin_project_snippet!, only: [:destroy] respond_to :html @@ -28,25 +28,22 @@ class Projects::SnippetsController < Projects::ApplicationController end def create - @snippet = @project.snippets.build(snippet_params) - @snippet.author = current_user - - if @snippet.save - redirect_to project_snippet_path(@project, @snippet) - else - respond_with(@snippet) - end + @snippet = CreateSnippetService.new(@project, current_user, + snippet_params).execute + respond_with(@snippet, + location: namespace_project_snippet_path(@project.namespace, + @project, @snippet)) end def edit end def update - if @snippet.update_attributes(snippet_params) - redirect_to project_snippet_path(@project, @snippet) - else - respond_with(@snippet) - end + UpdateSnippetService.new(project, current_user, @snippet, + snippet_params).execute + respond_with(@snippet, + location: namespace_project_snippet_path(@project.namespace, + @project, @snippet)) end def show @@ -60,7 +57,7 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet.destroy - redirect_to project_snippets_path(@project) + redirect_to namespace_project_snippets_path(@project.namespace, @project) end def raw @@ -68,7 +65,7 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet.content, type: 'text/plain; charset=utf-8', disposition: 'inline', - filename: @snippet.file_name + filename: @snippet.sanitized_file_name ) end diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 162ddef0fec..f565fbbbbc3 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -1,21 +1,22 @@ class Projects::TagsController < Projects::ApplicationController # Authorize - before_filter :require_non_empty_project - before_filter :authorize_download_code! - before_filter :authorize_push_code!, only: [:create] - before_filter :authorize_admin_project!, only: [:destroy] + before_action :require_non_empty_project + before_action :authorize_download_code! + before_action :authorize_push_code!, only: [:create] + before_action :authorize_admin_project!, only: [:destroy] def index sorted = VersionSorter.rsort(@repository.tag_names) - @tags = Kaminari.paginate_array(sorted).page(params[:page]).per(30) + @tags = Kaminari.paginate_array(sorted).page(params[:page]).per(PER_PAGE) end def create result = CreateTagService.new(@project, current_user). execute(params[:tag_name], params[:ref], params[:message]) + if result[:status] == :success @tag = result[:tag] - redirect_to project_tags_path(@project) + redirect_to namespace_project_tags_path(@project.namespace, @project) else @error = result[:message] render action: 'new' @@ -23,14 +24,13 @@ class Projects::TagsController < Projects::ApplicationController end def destroy - tag = @repository.find_tag(params[:id]) - - if tag && @repository.rm_tag(tag.name) - Event.create_ref_event(@project, current_user, tag, 'rm', 'refs/tags') - end + DeleteTagService.new(project, current_user).execute(params[:id]) respond_to do |format| - format.html { redirect_to project_tags_path } + format.html do + redirect_to namespace_project_tags_path(@project.namespace, + @project) + end format.js end end diff --git a/app/controllers/projects/team_members_controller.rb b/app/controllers/projects/team_members_controller.rb deleted file mode 100644 index 0791e6080fb..00000000000 --- a/app/controllers/projects/team_members_controller.rb +++ /dev/null @@ -1,74 +0,0 @@ -class Projects::TeamMembersController < Projects::ApplicationController - # Authorize - before_filter :authorize_admin_project!, except: :leave - - layout "project_settings" - - def index - @group = @project.group - @project_members = @project.project_members.order('access_level DESC') - end - - def new - @user_project_relation = @project.project_members.new - end - - def create - users = User.where(id: params[:user_ids].split(',')) - - @project.team << [users, params[:access_level]] - - if params[:redirect_to] - redirect_to params[:redirect_to] - else - redirect_to project_team_index_path(@project) - end - end - - def update - @user_project_relation = @project.project_members.find_by(user_id: member) - @user_project_relation.update_attributes(member_params) - - unless @user_project_relation.valid? - flash[:alert] = "User should have at least one role" - end - redirect_to project_team_index_path(@project) - end - - def destroy - @user_project_relation = @project.project_members.find_by(user_id: member) - @user_project_relation.destroy - - respond_to do |format| - format.html { redirect_to project_team_index_path(@project) } - format.js { render nothing: true } - end - end - - def leave - @project.project_members.find_by(user_id: current_user).destroy - - respond_to do |format| - format.html { redirect_to :back } - format.js { render nothing: true } - end - end - - def apply_import - giver = Project.find(params[:source_project_id]) - status = @project.team.import(giver) - notice = status ? "Successfully imported" : "Import failed" - - redirect_to project_team_index_path(project), notice: notice - end - - protected - - def member - @member ||= User.find_by(username: params[:id]) - end - - def member_params - params.require(:project_member).permit(:user_id, :access_level) - end -end diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 4d033b36848..b659e15f242 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -1,10 +1,18 @@ # Controller for viewing a repository's file structure -class Projects::TreeController < Projects::BaseTreeController - def show +class Projects::TreeController < Projects::ApplicationController + include ExtractsPath + + before_action :require_non_empty_project, except: [:new, :create] + before_action :assign_ref_vars + before_action :authorize_download_code! + def show if tree.entries.empty? if @repository.blob_at(@commit.id, @path) - redirect_to project_blob_path(@project, File.join(@ref, @path)) and return + redirect_to( + namespace_project_blob_path(@project.namespace, @project, + File.join(@ref, @path)) + ) and return else return not_found! end diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb new file mode 100644 index 00000000000..71ecc20dd95 --- /dev/null +++ b/app/controllers/projects/uploads_controller.rb @@ -0,0 +1,51 @@ +class Projects::UploadsController < Projects::ApplicationController + skip_before_action :authenticate_user!, :reject_blocked!, :project, + :repository, if: -> { action_name == 'show' && image? } + + def create + link_to_file = ::Projects::UploadService.new(project, params[:file]). + execute + + respond_to do |format| + if link_to_file + format.json do + render json: { link: link_to_file } + end + else + format.json do + render json: 'Invalid file.', status: :unprocessable_entity + end + end + end + end + + def show + return not_found! if uploader.nil? || !uploader.file.exists? + + disposition = uploader.image? ? 'inline' : 'attachment' + send_file uploader.file.path, disposition: disposition + end + + def uploader + return @uploader if defined?(@uploader) + + namespace = params[:namespace_id] + id = params[:project_id] + + file_project = Project.find_with_namespace("#{namespace}/#{id}") + + if file_project.nil? + @uploader = nil + return + end + + @uploader = FileUploader.new(file_project, params[:secret]) + @uploader.retrieve_from_store!(params[:filename]) + + @uploader + end + + def image? + uploader && uploader.file.exists? && uploader.image? + end +end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 0e03956e738..36ef86e1909 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -1,13 +1,14 @@ require 'project_wiki' class Projects::WikisController < Projects::ApplicationController - before_filter :authorize_read_wiki! - before_filter :authorize_write_wiki!, only: [:edit, :create, :history] - before_filter :authorize_admin_wiki!, only: :destroy - before_filter :load_project_wiki + before_action :authorize_read_wiki! + before_action :authorize_write_wiki!, only: [:edit, :create, :history] + before_action :authorize_admin_wiki!, only: :destroy + before_action :load_project_wiki + include WikiHelper def pages - @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(30) + @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(PER_PAGE) end def show @@ -16,16 +17,16 @@ class Projects::WikisController < Projects::ApplicationController if @page render 'show' elsif file = @project_wiki.find_file(params[:id], params[:version_id]) - if file.on_disk? - send_file file.on_disk_path, disposition: 'inline' - else - send_data( - file.raw_data, - type: file.mime_type, - disposition: 'inline', - filename: file.name - ) - end + if file.on_disk? + send_file file.on_disk_path, disposition: 'inline' + else + send_data( + file.raw_data, + type: file.mime_type, + disposition: 'inline', + filename: file.name + ) + end else return render('empty') unless can?(current_user, :write_wiki, @project) @page = WikiPage.new(@project_wiki) @@ -45,7 +46,10 @@ class Projects::WikisController < Projects::ApplicationController return render('empty') unless can?(current_user, :write_wiki, @project) if @page.update(content, format, message) - redirect_to [@project, @page], notice: 'Wiki was successfully updated.' + redirect_to( + namespace_project_wiki_path(@project.namespace, @project, @page), + notice: 'Wiki was successfully updated.' + ) else render 'edit' end @@ -55,7 +59,10 @@ class Projects::WikisController < Projects::ApplicationController @page = WikiPage.new(@project_wiki) if @page.create(wiki_params) - redirect_to project_wiki_path(@project, @page), notice: 'Wiki was successfully updated.' + redirect_to( + namespace_project_wiki_path(@project.namespace, @project, @page), + notice: 'Wiki was successfully updated.' + ) else render action: "edit" end @@ -65,7 +72,10 @@ class Projects::WikisController < Projects::ApplicationController @page = @project_wiki.find_page(params[:id]) unless @page - redirect_to(project_wiki_path(@project, :home), notice: "Page not found") + redirect_to( + namespace_project_wiki_path(@project.namespace, @project, :home), + notice: "Page not found" + ) end end @@ -73,7 +83,10 @@ class Projects::WikisController < Projects::ApplicationController @page = @project_wiki.find_page(params[:id]) @page.delete if @page - redirect_to project_wiki_path(@project, :home), notice: "Page was successfully deleted" + redirect_to( + namespace_project_wiki_path(@project.namespace, @project, :home), + notice: "Page was successfully deleted" + ) end def git_access @@ -88,7 +101,7 @@ class Projects::WikisController < Projects::ApplicationController @project_wiki.wiki rescue ProjectWiki::CouldNotCreateWikiError => ex flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." - redirect_to @project + redirect_to project_path(@project) return false end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index fbd9e5f2a5b..be5968cd7b0 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,27 +1,31 @@ class ProjectsController < ApplicationController - skip_before_filter :authenticate_user!, only: [:show] - before_filter :project, except: [:new, :create] - before_filter :repository, except: [:new, :create] + prepend_before_filter :render_go_import, only: [:show] + skip_before_action :authenticate_user!, only: [:show] + before_action :project, except: [:new, :create] + before_action :repository, except: [:new, :create] # Authorize - before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive] + before_action :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive] + before_action :event_filter, only: :show - layout 'navless', only: [:new, :create, :fork] - before_filter :set_title, only: [:new, :create] + layout :determine_layout def new @project = Project.new end def edit - render 'edit', layout: "project_settings" + render 'edit' end def create @project = ::Projects::CreateService.new(current_user, project_params).execute if @project.saved? - redirect_to project_path(@project), notice: 'Project was successfully created.' + redirect_to( + project_path(@project), + notice: 'Project was successfully created.' + ) else render 'new' end @@ -33,47 +37,59 @@ class ProjectsController < ApplicationController respond_to do |format| if status flash[:notice] = 'Project was successfully updated.' - format.html { redirect_to edit_project_path(@project), notice: 'Project was successfully updated.' } + format.html do + redirect_to( + edit_project_path(@project), + notice: 'Project was successfully updated.' + ) + end format.js else - format.html { render "edit", layout: "project_settings" } + format.html { render 'edit' } format.js end end end def transfer - ::Projects::TransferService.new(project, current_user, project_params).execute + transfer_params = params.permit(:new_namespace_id) + ::Projects::TransferService.new(project, current_user, transfer_params).execute + if @project.errors[:namespace_id].present? + flash[:alert] = @project.errors[:namespace_id].first + end end def show if @project.import_in_progress? - redirect_to project_import_path(@project) + redirect_to namespace_project_import_path(@project.namespace, @project) return end - limit = (params[:limit] || 20).to_i - @events = @project.events.recent - @events = event_filter.apply_filter(@events) - @events = @events.limit(limit).offset(params[:offset] || 0) - @show_star = !(current_user && current_user.starred?(@project)) respond_to do |format| format.html do if @project.repository_exists? if @project.empty_repo? - render "projects/empty", layout: user_layout + render 'projects/empty' else @last_push = current_user.recent_push(@project.id) if current_user - render :show, layout: user_layout + render :show end else - render "projects/no_repo", layout: user_layout + render 'projects/no_repo' end end - format.json { pager_json("events/_events", @events.count) } + format.json do + load_events + pager_json('events/_events', @events.count) + end + + format.atom do + load_events + render layout: false + end end end @@ -81,33 +97,32 @@ class ProjectsController < ApplicationController return access_denied! unless can?(current_user, :remove_project, @project) ::Projects::DestroyService.new(@project, current_user, {}).execute + flash[:alert] = 'Project deleted.' - respond_to do |format| - format.html do - flash[:alert] = "Project deleted." - - if request.referer.include?("/admin") - redirect_to admin_projects_path - else - redirect_to projects_dashboard_path - end - end + if request.referer.include?('/admin') + redirect_to admin_namespaces_projects_path + else + redirect_to dashboard_path end + rescue Projects::DestroyService::DestroyError => ex + redirect_to edit_project_path(@project), alert: ex.message end def autocomplete_sources note_type = params['type'] note_id = params['type_id'] - participants = ::Projects::ParticipantsService.new(@project).execute(note_type, note_id) + autocomplete = ::Projects::AutocompleteService.new(@project) + participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) + @suggestions = { - emojis: Emoji.names.map { |e| { name: e, path: view_context.image_url("emoji/#{e}.png") } }, - issues: @project.issues.select([:iid, :title, :description]), - mergerequests: @project.merge_requests.select([:iid, :title, :description]), + emojis: autocomplete_emojis, + issues: autocomplete.issues, + mergerequests: autocomplete.merge_requests, members: participants } respond_to do |format| - format.json { render :json => @suggestions } + format.json { render json: @suggestions } end end @@ -116,7 +131,7 @@ class ProjectsController < ApplicationController @project.archive! respond_to do |format| - format.html { redirect_to @project } + format.html { redirect_to project_path(@project) } end end @@ -125,19 +140,7 @@ class ProjectsController < ApplicationController @project.unarchive! respond_to do |format| - format.html { redirect_to @project } - end - end - - def upload_image - link_to_image = ::Projects::ImageService.new(repository, params, root_url).execute - - respond_to do |format| - if link_to_image - format.json { render json: { link: link_to_image } } - else - format.json { render json: "Invalid file.", status: :unprocessable_entity } - end + format.html { redirect_to project_path(@project) } end end @@ -147,30 +150,65 @@ class ProjectsController < ApplicationController render json: { star_count: @project.star_count } end - private + def markdown_preview + text = params[:text] - def upload_path - base_dir = FileUploader.generate_dir - File.join(repository.path_with_namespace, base_dir) - end + ext = Gitlab::ReferenceExtractor.new(@project, current_user) + ext.analyze(text) - def accepted_images - %w(png jpg jpeg gif) + render json: { + body: view_context.markdown(text), + references: { + users: ext.users.map(&:username) + } + } end - def set_title - @title = 'New Project' + private + + def determine_layout + if [:new, :create].include?(action_name.to_sym) + 'application' + elsif [:edit, :update].include?(action_name.to_sym) + 'project_settings' + else + 'project' + end end - def user_layout - current_user ? "projects" : "public_projects" + def load_events + @events = @project.events.recent + @events = event_filter.apply_filter(@events).with_associations + limit = (params[:limit] || 20).to_i + @events = @events.limit(limit).offset(params[:offset] || 0) end def project_params params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, - :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id + :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar ) end + + def autocomplete_emojis + Rails.cache.fetch("autocomplete-emoji-#{Gemojione::VERSION}") do + Emoji.emojis.map do |name, emoji| + { + name: name, + path: view_context.image_url("emoji/#{emoji["unicode"]}.png") + } + end + end + end + + def render_go_import + return unless params["go-get"] == "1" + + @namespace = params[:namespace_id] + @id = params[:project_id] || params[:id] + @id = @id.gsub(/\.git\Z/, "") + + render "go_import", layout: false + end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 6d3214b70a8..6ccc7934f2f 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,8 +1,12 @@ class RegistrationsController < Devise::RegistrationsController - before_filter :signup_enabled? + before_action :signup_enabled? + + def new + redirect_to(new_user_session_path) + end def destroy - current_user.destroy + DeleteUserService.new.execute(current_user) respond_to do |format| format.html { redirect_to new_user_session_path, notice: "Account successfully removed." } @@ -15,18 +19,20 @@ class RegistrationsController < Devise::RegistrationsController super end - def after_sign_up_path_for(resource) + def after_sign_up_path_for(_resource) new_user_session_path end - def after_inactive_sign_up_path_for(resource) + def after_inactive_sign_up_path_for(_resource) new_user_session_path end private def signup_enabled? - redirect_to new_user_session_path unless Gitlab.config.gitlab.signup_enabled + unless current_application_settings.signup_enabled? + redirect_to(new_user_session_path) + end end def sign_up_params diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb new file mode 100644 index 00000000000..fdfe00dc135 --- /dev/null +++ b/app/controllers/root_controller.rb @@ -0,0 +1,28 @@ +# RootController +# +# This controller exists solely to handle requests to `root_url`. When a user is +# logged in and has customized their `dashboard` setting, they will be +# redirected to their preferred location. +# +# For users who haven't customized the setting, we simply delegate to +# `DashboardController#show`, which is the default. +class RootController < DashboardController + before_action :redirect_to_custom_dashboard, only: [:show] + + def show + super + end + + private + + def redirect_to_custom_dashboard + return unless current_user + + case current_user.dashboard + when 'stars' + redirect_to starred_dashboard_projects_path + else + return + end + end +end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 55926a1ed22..4e2ea6c5710 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,41 +1,58 @@ class SearchController < ApplicationController include SearchHelper + layout 'search' + def show - @project = Project.find_by(id: params[:project_id]) if params[:project_id].present? - @group = Group.find_by(id: params[:group_id]) if params[:group_id].present? - @scope = params[:scope] - @show_snippets = params[:snippets].eql? 'true' + return if params[:search].nil? || params[:search].blank? - @search_results = if @project - return access_denied! unless can?(current_user, :download_code, @project) + @search_term = params[:search] - unless %w(blobs notes issues merge_requests wiki_blobs). - include?(@scope) - @scope = 'blobs' - end + if params[:project_id].present? + @project = Project.find_by(id: params[:project_id]) + @project = nil unless can?(current_user, :download_code, @project) + end - Search::ProjectService.new(@project, current_user, params).execute - elsif @show_snippets - unless %w(snippet_blobs snippet_titles).include?(@scope) - @scope = 'snippet_blobs' - end + if params[:group_id].present? + @group = Group.find_by(id: params[:group_id]) + @group = nil unless can?(current_user, :read_group, @group) + end - Search::SnippetService.new(current_user, params).execute - else - unless %w(projects issues merge_requests).include?(@scope) - @scope = 'projects' - end + @scope = params[:scope] + @show_snippets = params[:snippets].eql? 'true' - Search::GlobalService.new(current_user, params).execute - end + @search_results = + if @project + unless %w(blobs notes issues merge_requests wiki_blobs). + include?(@scope) + @scope = 'blobs' + end + + Search::ProjectService.new(@project, current_user, params).execute + elsif @show_snippets + unless %w(snippet_blobs snippet_titles).include?(@scope) + @scope = 'snippet_blobs' + end + + Search::SnippetService.new(current_user, params).execute + else + unless %w(projects issues merge_requests).include?(@scope) + @scope = 'projects' + end + Search::GlobalService.new(current_user, params).execute + end @objects = @search_results.objects(@scope, params[:page]) end def autocomplete term = params[:term] - @project = Project.find(params[:project_id]) if params[:project_id].present? + + if params[:project_id].present? + @project = Project.find_by(id: params[:project_id]) + @project = nil unless can?(current_user, :read_project, @project) + end + @ref = params[:project_ref] if params[:project_ref].present? render json: search_autocomplete_opts(term).to_json diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 5ced98152a5..7577fc96d6d 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,20 +1,25 @@ class SessionsController < Devise::SessionsController + include AuthenticatesWithTwoFactor + + prepend_before_action :authenticate_with_two_factor, only: [:create] + before_action :auto_sign_in_with_provider, only: [:new] def new - redirect_path = if request.referer.present? && (params['redirect_to_referer'] == 'yes') - referer_uri = URI(request.referer) - if referer_uri.host == Gitlab.config.gitlab.host - referer_uri.path - else - request.fullpath - end - else - request.fullpath - end + redirect_path = + if request.referer.present? && (params['redirect_to_referer'] == 'yes') + referer_uri = URI(request.referer) + if referer_uri.host == Gitlab.config.gitlab.host + referer_uri.path + else + request.fullpath + end + else + request.fullpath + end # Prevent a 'you are already signed in' message directly after signing: # we should never redirect to '/users/sign_in' after signing in successfully. - unless redirect_path == '/users/sign_in' + unless redirect_path == new_user_session_path store_location_for(:redirect, redirect_path) end @@ -26,6 +31,68 @@ class SessionsController < Devise::SessionsController end def create - super + super do |resource| + # User has successfully signed in, so clear any unused reset token + if resource.reset_password_token.present? + resource.update_attributes(reset_password_token: nil, + reset_password_sent_at: nil) + end + end + end + + private + + def user_params + params.require(:user).permit(:login, :password, :remember_me, :otp_attempt) + end + + def find_user + if user_params[:login] + User.by_login(user_params[:login]) + elsif user_params[:otp_attempt] && session[:otp_user_id] + User.find(session[:otp_user_id]) + end + end + + def authenticate_with_two_factor + user = self.resource = find_user + + return unless user && user.two_factor_enabled? + + 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? + + # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is + # registered or no alert at all. In case of another alert (such as a blocked user), it is safer + # to do nothing to prevent redirection loops with certain Omniauth providers. + return unless flash[:alert].blank? || flash[:alert] == I18n.t('devise.failure.unauthenticated') + + # Prevent alert from popping up on the first page shown after authentication. + flash[:alert] = nil + + redirect_to omniauth_authorize_path(:user, provider.to_sym) + end + + def valid_otp_attempt?(user) + user.valid_otp?(user_params[:otp_attempt]) || + user.invalidate_otp_backup_code!(user_params[:otp_attempt]) end end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index bf3312fedc8..cf672c5c093 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,39 +1,36 @@ class SnippetsController < ApplicationController - before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow modify snippet - before_filter :authorize_modify_snippet!, only: [:edit, :update] + before_action :authorize_modify_snippet!, only: [:edit, :update] # Allow destroy snippet - before_filter :authorize_admin_snippet!, only: [:destroy] + before_action :authorize_admin_snippet!, only: [:destroy] - before_filter :set_title - - skip_before_filter :authenticate_user!, only: [:index, :user_index, :show, :raw] + skip_before_action :authenticate_user!, only: [:index, :user_index, :show, :raw] + layout 'snippets' respond_to :html - layout :determine_layout - def index - @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(20) - end - - def user_index - @user = User.find_by(username: params[:username]) - - render_404 and return unless @user - - @snippets = SnippetsFinder.new.execute(current_user, { - filter: :by_user, - user: @user, - scope: params[:scope]}). - page(params[:page]).per(20) - - if @user == current_user - render 'current_user_index' + if params[:username].present? + @user = User.find_by(username: params[:username]) + + render_404 and return unless @user + + @snippets = SnippetsFinder.new.execute(current_user, { + filter: :by_user, + user: @user, + scope: params[:scope] }). + page(params[:page]).per(PER_PAGE) + + if @user == current_user + render 'current_user_index' + else + render 'user_index' + end else - render 'user_index' + @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(PER_PAGE) end end @@ -42,25 +39,19 @@ class SnippetsController < ApplicationController end def create - @snippet = PersonalSnippet.new(snippet_params) - @snippet.author = current_user + @snippet = CreateSnippetService.new(nil, current_user, + snippet_params).execute - if @snippet.save - redirect_to snippet_path(@snippet) - else - respond_with @snippet - end + respond_with @snippet.becomes(Snippet) end def edit end def update - if @snippet.update_attributes(snippet_params) - redirect_to snippet_path(@snippet) - else - respond_with @snippet - end + UpdateSnippetService.new(nil, current_user, @snippet, + snippet_params).execute + respond_with @snippet.becomes(Snippet) end def show @@ -79,7 +70,7 @@ class SnippetsController < ApplicationController @snippet.content, type: 'text/plain; charset=utf-8', disposition: 'inline', - filename: @snippet.file_name + filename: @snippet.sanitized_file_name ) end @@ -104,15 +95,7 @@ class SnippetsController < ApplicationController return render_404 unless can?(current_user, :admin_personal_snippet, @snippet) end - def set_title - @title = 'Snippets' - end - def snippet_params params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level) end - - def determine_layout - current_user ? 'navless' : 'public_users' - end end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb new file mode 100644 index 00000000000..28536e359e5 --- /dev/null +++ b/app/controllers/uploads_controller.rb @@ -0,0 +1,71 @@ +class UploadsController < ApplicationController + skip_before_action :authenticate_user! + before_action :find_model, :authorize_access! + + def show + uploader = @model.send(upload_mount) + + unless uploader.file_storage? + return redirect_to uploader.url + end + + unless uploader.file && uploader.file.exists? + return not_found! + end + + disposition = uploader.image? ? 'inline' : 'attachment' + send_file uploader.file.path, disposition: disposition + end + + private + + def find_model + unless upload_model && upload_mount + return not_found! + end + + @model = upload_model.find(params[:id]) + end + + def authorize_access! + authorized = + case @model + when Project + can?(current_user, :read_project, @model) + when Group + can?(current_user, :read_group, @model) + when Note + can?(current_user, :read_project, @model.project) + else + # No authentication required for user avatars. + true + end + + return if authorized + + if current_user + not_found! + else + authenticate_user! + end + end + + def upload_model + upload_models = { + "user" => User, + "project" => Project, + "note" => Note, + "group" => Group + } + + upload_models[params[:model]] + end + + def upload_mount + upload_mounts = %w(avatar attachment file) + + if upload_mounts.include?(params[:mounted_as]) + params[:mounted_as] + end + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0b442f5383a..2bb5c338cf6 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,35 +1,85 @@ class UsersController < ApplicationController - skip_before_filter :authenticate_user!, only: [:show] - layout :determine_layout + skip_before_action :authenticate_user! + before_action :set_user def show + @contributed_projects = contributed_projects.joined(@user). + reject(&:forked?) + + @projects = @user.personal_projects. + where(id: authorized_projects_ids).includes(:namespace) + + # Collect only groups common for both users + @groups = @user.groups & GroupsFinder.new.execute(current_user) + + respond_to do |format| + format.html + + format.atom do + load_events + render layout: false + end + + format.json do + load_events + pager_json("events/_events", @events.count) + 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 + + def calendar_activities + @calendar_date = Date.parse(params[:date]) rescue nil + @events = [] + + if @calendar_date + @events = contributions_calendar.events_by_date(@calendar_date) + end + + render 'calendar_activities', layout: false + end + + private + + def set_user @user = User.find_by_username!(params[:username]) unless current_user || @user.public_profile? return authenticate_user! end + end + def authorized_projects_ids # Projects user can view - authorized_projects_ids = ProjectsFinder.new.execute(current_user).pluck(:id) + @authorized_projects_ids ||= + ProjectsFinder.new.execute(current_user).pluck(:id) + end - @projects = @user.personal_projects. - where(id: authorized_projects_ids) + def contributed_projects + @contributed_projects = Project. + where(id: authorized_projects_ids & @user.contributed_projects_ids). + includes(:namespace) + end - # Collect only groups common for both users - @groups = @user.groups & GroupsFinder.new.execute(current_user) + def contributions_calendar + @contributions_calendar ||= Gitlab::ContributionsCalendar. + new(contributed_projects.reject(&:forked?), @user) + end + def load_events # Get user activity feed for projects common for both users @events = @user.recent_events. - where(project_id: authorized_projects_ids).limit(20) + where(project_id: authorized_projects_ids). + with_associations - @title = @user.name - end - - def determine_layout - if current_user - 'navless' - else - 'public_users' - end + @events = @events.limit(20).offset(params[:offset] || 0) end end diff --git a/app/finders/README.md b/app/finders/README.md index 1f46518d230..1a1c69dea38 100644 --- a/app/finders/README.md +++ b/app/finders/README.md @@ -16,7 +16,7 @@ issues = project.issues_for_user_filtered_by(user, params) Better use this: ```ruby -issues = IssuesFinder.new.execute(project, user, filter) +issues = IssuesFinder.new(project, user, filter).execute ``` It will help keep models thiner. diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index d0574240511..0bed2115dc7 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -19,12 +19,16 @@ require_relative 'projects_finder' class IssuableFinder + NONE = '0' + attr_accessor :current_user, :params - def execute(current_user, params) + def initialize(current_user, params) @current_user = current_user @params = params + end + def execute items = init_collection items = by_scope(items) items = by_state(items) @@ -33,17 +37,89 @@ class IssuableFinder items = by_search(items) items = by_milestone(items) items = by_assignee(items) + items = by_author(items) items = by_label(items) items = sort(items) end + def group + return @group if defined?(@group) + + @group = + if params[:group_id].present? + Group.find(params[:group_id]) + else + nil + end + end + + def project + return @project if defined?(@project) + + @project = + if params[:project_id].present? + Project.find(params[:project_id]) + else + nil + end + end + + def search + params[:search].presence + end + + def milestones? + params[:milestone_title].present? + end + + def milestones + return @milestones if defined?(@milestones) + + @milestones = + if milestones? && params[:milestone_title] != NONE + Milestone.where(title: params[:milestone_title]) + else + nil + end + end + + def assignee? + params[:assignee_id].present? + end + + def assignee + return @assignee if defined?(@assignee) + + @assignee = + if assignee? && params[:assignee_id] != NONE + User.find(params[:assignee_id]) + else + nil + end + end + + def author? + params[:author_id].present? + end + + def author + return @author if defined?(@author) + + @author = + if author? && params[:author_id] != NONE + User.find(params[:author_id]) + else + nil + end + end + private def init_collection table_name = klass.table_name if project - if project.public? || (current_user && current_user.can?(:read_project, project)) + if Ability.abilities.allowed?(current_user, :read_project, project) project.send(table_name) else [] @@ -72,6 +148,10 @@ class IssuableFinder case params[:state] when 'closed' items.closed + when 'rejected' + items.respond_to?(:rejected) ? items.rejected : items.closed + when 'merged' + items.respond_to?(:merged) ? items.merged : items.closed when 'all' items when 'opened' @@ -82,25 +162,19 @@ class IssuableFinder end def by_group(items) - if params[:group_id].present? - items = items.of_group(Group.find(params[:group_id])) - end + items = items.of_group(group) if group items end def by_project(items) - if params[:project_id].present? - items = items.of_projects(params[:project_id]) - end + items = items.of_projects(project.id) if project items end def by_search(items) - if params[:search].present? - items = items.search(params[:search]) - end + items = items.search(search) if search items end @@ -110,16 +184,24 @@ class IssuableFinder end def by_milestone(items) - if params[:milestone_id].present? - items = items.where(milestone_id: (params[:milestone_id] == '0' ? nil : params[:milestone_id])) + if milestones? + items = items.where(milestone_id: milestones.try(:pluck, :id)) end items end def by_assignee(items) - if params[:assignee_id].present? - items = items.where(assignee_id: (params[:assignee_id] == '0' ? nil : params[:assignee_id])) + if assignee? + items = items.where(assignee_id: assignee.try(:id)) + end + + items + end + + def by_author(items) + if author? + items = items.where(author_id: author.try(:id)) end items @@ -139,10 +221,6 @@ class IssuableFinder items end - def project - Project.where(id: params[:project_id]).first if params[:project_id].present? - end - def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index bef82d7f0fd..ab252821b52 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -7,20 +7,21 @@ class NotesFinder # Default to 0 to remain compatible with old clients last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i) - notes = case target_type - when "commit" - project.notes.for_commit_id(target_id).not_inline.fresh - when "issue" - project.issues.find(target_id).notes.inc_author.fresh - when "merge_request" - project.merge_requests.find(target_id).mr_and_commit_notes.inc_author.fresh - when "snippet", "project_snippet" - project.snippets.find(target_id).notes.fresh - else - raise 'invalid target_type' - end + notes = + case target_type + when "commit" + project.notes.for_commit_id(target_id).not_inline + when "issue" + project.issues.find(target_id).notes.inc_author + when "merge_request" + project.merge_requests.find(target_id).mr_and_commit_notes.inc_author + when "snippet", "project_snippet" + project.snippets.find(target_id).notes + else + raise 'invalid target_type' + end # Use overlapping intervals to avoid worrying about race conditions - notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP) + notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh end end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 4b0c69f2d2f..07b5759443b 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -40,7 +40,7 @@ class SnippetsFinder when 'are_public' then snippets.are_public else - snippets + snippets end else snippets.public_and_internal diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb index 32d7968924a..a79bd47d986 100644 --- a/app/finders/trending_projects_finder.rb +++ b/app/finders/trending_projects_finder.rb @@ -8,7 +8,7 @@ class TrendingProjectsFinder # for period of time - ex. month projects.joins(:notes).where('notes.created_at > ?', start_date). select("projects.*, count(notes.id) as ncount"). - group("projects.id").order("ncount DESC") + group("projects.id").reorder("ncount DESC") end private diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 96e5d43a369..14df8d4cbd7 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -14,4 +14,8 @@ module AppearancesHelper def brand_text nil end + + def brand_header_logo + image_tag 'logo.svg' + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 021bd0a494c..9889c995c74 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,25 +2,6 @@ require 'digest/md5' require 'uri' module ApplicationHelper - COLOR_SCHEMES = { - 1 => 'white', - 2 => 'dark', - 3 => 'solarized-dark', - 4 => 'monokai', - } - COLOR_SCHEMES.default = 'white' - - # Helper method to access the COLOR_SCHEMES - # - # The keys are the `color_scheme_ids` - # The values are the `name` of the scheme. - # - # The preview images are `name-scheme-preview.png` - # The stylesheets should use the css class `.name` - def color_schemes - COLOR_SCHEMES.freeze - end - # Check if a particular controller is the current one # # args - One or more controller names to check @@ -49,12 +30,39 @@ module ApplicationHelper args.any? { |v| v.to_s.downcase == action_name } end - def group_icon(group_path) - group = Group.find_by(path: group_path) - if group && group.avatar.present? - group.avatar.url - else - image_path('no_group_avatar.png') + def project_icon(project_id, options = {}) + project = + if project_id.is_a?(Project) + project = project_id + else + Project.find_with_namespace(project_id) + end + + if project.avatar_url + image_tag project.avatar_url, options + else # generated icon + project_identicon(project, options) + end + end + + def project_identicon(project, options = {}) + allowed_colors = { + red: 'FFEBEE', + purple: 'F3E5F5', + indigo: 'E8EAF6', + blue: 'E3F2FD', + teal: 'E0F2F1', + orange: 'FBE9E7', + gray: 'EEEEEE' + } + + options[:class] ||= '' + options[:class] << ' identicon' + bg_key = project.id % 7 + style = "background-color: ##{ allowed_colors.values[bg_key] }; color: #555" + + content_tag(:div, class: options[:class], style: style) do + project.name[0, 1].upcase end end @@ -81,24 +89,24 @@ module ApplicationHelper if project.repo_exists? time_ago_with_tooltip(project.repository.commit.committed_date) else - "Never" + 'Never' end rescue - "Never" + 'Never' end def grouped_options_refs repository = @project.repository options = [ - ["Branches", repository.branch_names], - ["Tags", VersionSorter.rsort(repository.tag_names)] + ['Branches', repository.branch_names], + ['Tags', VersionSorter.rsort(repository.tag_names)] ] # If reference is commit id - we should add it to branch/tag selectbox if(@ref && !options.flatten.include?(@ref) && - @ref =~ /^[0-9a-zA-Z]{6,52}$/) - options << ["Commit", [@ref]] + @ref =~ /\A[0-9a-zA-Z]{6,52}\z/) + options << ['Commit', [@ref]] end grouped_options_for_select(options, @ref || @project.default_branch) @@ -110,14 +118,6 @@ module ApplicationHelper Emoji.names.to_s end - def app_theme - Gitlab::Theme.css_class_by_id(current_user.try(:theme_id)) - end - - def user_color_scheme_class - COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user) - end - # Define whenever show last push event # with suggestion to create MR def show_last_push_widget?(event) @@ -142,21 +142,15 @@ module ApplicationHelper Digest::SHA1.hexdigest string end - def authbutton(provider, size = 64) - file_name = "#{provider.to_s.split('_').first}_#{size}.png" - image_tag(image_path("authbuttons/#{file_name}"), alt: "Sign in with #{provider.to_s.titleize}") - end - def simple_sanitize(str) sanitize(str, tags: %w(a span)) end - def body_data_page path = controller.controller_path.split('/') namespace = path.first if path.second - [namespace, controller.controller_name, controller.action_name].compact.join(":") + [namespace, controller.controller_name, controller.action_name].compact.join(':') end # shortcut for gitlab config @@ -171,13 +165,13 @@ module ApplicationHelper def search_placeholder if @project && @project.persisted? - "Search in this project" + 'Search in this project' elsif @snippet || @snippets || @show_snippets 'Search snippets' elsif @group && @group.persisted? - "Search in this group" + 'Search in this group' else - "Search" + 'Search' end end @@ -185,90 +179,130 @@ module ApplicationHelper BroadcastMessage.current end - def highlight_js(&block) - string = capture(&block) - - content_tag :div, class: "highlighted-data #{user_color_scheme_class}" do - content_tag :div, class: 'highlight' do - content_tag :pre do - content_tag :code do - string.html_safe - end - end - end - end - end + # Render a `time` element with Javascript-based relative date and tooltip + # + # time - Time object + # placement - Tooltip placement String (default: "top") + # html_class - Custom class for `time` element (default: "time_ago") + # skip_js - When true, exclude the `script` tag (default: false) + # + # By default also includes a `script` element with Javascript necessary to + # initialize the `timeago` jQuery extension. If this method is called many + # times, for example rendering hundreds of commits, it's advisable to disable + # this behavior using the `skip_js` argument and re-initializing `timeago` + # manually once all of the elements have been rendered. + # + # A `js-timeago` class is always added to the element, even when a custom + # `html_class` argument is provided. + # + # Returns an HTML-safe String + def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false) + element = content_tag :time, time.to_s, + class: "#{html_class} js-timeago", + datetime: time.getutc.iso8601, + title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), + data: { toggle: 'tooltip', placement: placement } - def time_ago_with_tooltip(date, placement = 'top', html_class = 'time_ago') - capture_haml do - haml_tag :time, date.to_s, - class: html_class, datetime: date.getutc.iso8601, title: date.stamp("Aug 21, 2011 9:23pm"), - data: { toggle: 'tooltip', placement: placement } + element += javascript_tag "$('.js-timeago').timeago()" unless skip_js - haml_tag :script, "$('." + html_class + "').timeago().tooltip()" - end.html_safe + element end def render_markup(file_name, file_content) - GitHub::Markup.render(file_name, file_content). - force_encoding(file_content.encoding).html_safe + if gitlab_markdown?(file_name) + Haml::Helpers.preserve(markdown(file_content)) + elsif asciidoc?(file_name) + asciidoc(file_content) + else + GitHub::Markup.render(file_name, file_content). + force_encoding(file_content.encoding).html_safe + end rescue RuntimeError simple_format(file_content) end def markup?(filename) - Gitlab::MarkdownHelper.markup?(filename) + Gitlab::MarkupHelper.markup?(filename) end def gitlab_markdown?(filename) - Gitlab::MarkdownHelper.gitlab_markdown?(filename) + Gitlab::MarkupHelper.gitlab_markdown?(filename) end - def spinner(text = nil, visible = false) - css_class = "loading" - css_class << " hide" unless visible + def asciidoc?(filename) + Gitlab::MarkupHelper.asciidoc?(filename) + end - content_tag :div, class: css_class do - content_tag(:i, nil, class: 'fa fa-spinner fa-spin') + text - end + def promo_host + 'about.gitlab.com' end - def link_to(name = nil, options = nil, html_options = nil, &block) - begin - uri = URI(options) - host = uri.host - absolute_uri = uri.absolute? - rescue URI::InvalidURIError, ArgumentError - host = nil - absolute_uri = nil - end + def promo_url + 'https://' + promo_host + end - # Add "nofollow" only to external links - if host && host != Gitlab.config.gitlab.host && absolute_uri - if html_options - if html_options[:rel] - html_options[:rel] << " nofollow" - else - html_options.merge!(rel: "nofollow") - end - else - html_options = Hash.new - html_options[:rel] = "nofollow" + def page_filter_path(options = {}) + without = options.delete(:without) + + exist_opts = { + state: params[:state], + scope: params[:scope], + label_name: params[:label_name], + milestone_id: params[:milestone_id], + assignee_id: params[:assignee_id], + author_id: params[:author_id], + sort: params[:sort], + } + + options = exist_opts.merge(options) + + if without.present? + without.each do |key| + options.delete(key) end end - super + path = request.path + path << "?#{options.to_param}" + path end - def escaped_autolink(text) - auto_link ERB::Util.html_escape(text), link: :urls + def outdated_browser? + browser.ie? && browser.version.to_i < 10 end - def promo_host - 'about.gitlab.com' + def path_to_key(key, admin = false) + if admin + admin_user_key_path(@user, key) + else + profile_key_path(key) + end end - def promo_url - 'https://' + promo_host + def state_filters_text_for(entity, project) + titles = { + opened: "Open", + merged: "Accepted" + } + + entity_title = titles[entity] || entity.to_s.humanize + + count = + if project.nil? + nil + elsif current_controller?(:issues) + project.issues.send(entity).count + elsif current_controller?(:merge_requests) + project.merge_requests.send(entity).count + end + + html = content_tag :span, entity_title + + if count.present? + html += " " + html += content_tag :span, number_with_delimiter(count), class: 'badge' + end + + html.html_safe end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb new file mode 100644 index 00000000000..63c3ff5674d --- /dev/null +++ b/app/helpers/application_settings_helper.rb @@ -0,0 +1,42 @@ +module ApplicationSettingsHelper + def gravatar_enabled? + current_application_settings.gravatar_enabled? + end + + def twitter_sharing_enabled? + current_application_settings.twitter_sharing_enabled? + end + + def signup_enabled? + current_application_settings.signup_enabled? + end + + def signin_enabled? + current_application_settings.signin_enabled? + end + + def extra_sign_in_text + current_application_settings.sign_in_text + end + + def user_oauth_applications? + current_application_settings.user_oauth_applications + end + + # Return a group of checkboxes that use Bootstrap's button plugin for a + # toggle button effect. + def restricted_level_checkboxes(help_block_id) + Gitlab::VisibilityLevel.options.map do |name, level| + checked = restricted_visibility_levels(true).include?(level) + css_class = 'btn btn-primary' + css_class += ' active' if checked + checkbox_name = 'application_setting[restricted_visibility_levels][]' + + label_tag(checkbox_name, class: css_class) do + check_box_tag(checkbox_name, level, checked, + autocomplete: 'off', + 'aria-describedby' => help_block_id) + name + end + end + end +end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 420ac3f77c7..50df3801703 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,13 +1,74 @@ module BlobHelper - def highlightjs_class(blob_name) - if no_highlight_files.include?(blob_name.downcase) - 'no-highlight' - else - blob_name.downcase + def highlight(blob_name, blob_content, nowrap: false, continue: false) + @formatter ||= Rugments::Formatters::HTML.new( + nowrap: nowrap, + cssclass: 'code highlight', + lineanchors: true, + lineanchorsid: 'LC' + ) + + begin + @lexer ||= Rugments::Lexer.guess(filename: blob_name, source: blob_content).new + result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe + rescue + lexer = Rugments::Lexers::PlainText + result = @formatter.format(lexer.lex(blob_content)).html_safe end + + result end def no_highlight_files - %w(credits changelog copying copyright license authors) + %w(credits changelog news copying copyright license authors) + end + + def edit_blob_link(project, ref, path, options = {}) + blob = + begin + project.repository.blob_at(ref, path) + rescue + nil + end + + if blob && blob.text? + text = 'Edit' + after = options[:after] || '' + from_mr = options[:from_merge_request_id] + link_opts = {} + link_opts[:from_merge_request_id] = from_mr if from_mr + cls = 'btn btn-small' + if allowed_tree_edit?(project, ref) + link_to(text, + namespace_project_edit_blob_path(project.namespace, project, + tree_join(ref, path), + link_opts), + class: cls + ) + else + content_tag :span, text, class: cls + ' disabled' + end + after.html_safe + else + '' + end + end + + def leave_edit_message + "Leave edit mode?\nAll unsaved changes will be lost." + end + + def editing_preview_title(filename) + if Gitlab::MarkupHelper.previewable?(filename) + 'Preview' + else + 'Preview changes' + end + end + + # Return an image icon depending on the file mode and extension + # + # mode - File unix mode + # mode - File name + def blob_icon(mode, name) + icon("#{file_type_icon_class('file', mode, name)} fw") end end diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index 2ec2cc96157..d6eaa7d57bc 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -11,12 +11,7 @@ module BranchesHelper def can_push_branch?(project, branch_name) return false unless project.repository.branch_names.include?(branch_name) - action = if project.protected_branch?(branch_name) - :push_code_to_protected_branches - else - :push_code - end - - current_user.can?(action, project) + + ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name) end end diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index 29ff47663da..6484dca6b55 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -1,9 +1,16 @@ module BroadcastMessagesHelper def broadcast_styling(broadcast_message) - if(broadcast_message.color || broadcast_message.font) - "background-color:#{broadcast_message.color};color:#{broadcast_message.font}" - else - "" + styling = '' + + if broadcast_message.color.present? + styling << "background-color: #{broadcast_message.color}" + styling << '; ' if broadcast_message.font.present? end + + if broadcast_message.font.present? + styling << "color: #{broadcast_message.font}" + end + + styling end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 36adeadd8a5..d13d80be293 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -37,16 +37,26 @@ module CommitsHelper # Add the root project link and the arrow icon crumbs = content_tag(:li) do - link_to(@project.path, project_commits_path(@project, @ref)) + link_to( + @project.path, + namespace_project_commits_path(@project.namespace, @project, @ref) + ) end if @path parts = @path.split('/') parts.each_with_index do |part, i| - crumbs += content_tag(:li) do + crumbs << content_tag(:li) do # The text is just the individual part, but the link needs all the parts before it - link_to part, project_commits_path(@project, tree_join(@ref, parts[0..i].join('/'))) + link_to( + part, + namespace_project_commits_path( + @project.namespace, + @project, + tree_join(@ref, parts[0..i].join('/')) + ) + ) end end end @@ -62,18 +72,55 @@ module CommitsHelper # Returns the sorted alphabetically links to branches, separated by a comma def commit_branches_links(project, branches) - branches.sort.map { |branch| link_to(branch, project_tree_path(project, branch)) }.join(", ").html_safe + branches.sort.map do |branch| + link_to( + namespace_project_tree_path(project.namespace, project, branch) + ) do + content_tag :span, class: 'label label-gray' do + icon('code-fork') + ' ' + branch + end + end + end.join(" ").html_safe + end + + # Returns the sorted links to tags, separated by a comma + def commit_tags_links(project, tags) + sorted = VersionSorter.rsort(tags) + sorted.map do |tag| + link_to( + namespace_project_commits_path(project.namespace, project, + project.repository.find_tag(tag).name) + ) do + content_tag :span, class: 'label label-gray' do + icon('tag') + ' ' + tag + end + end + end.join(" ").html_safe end def link_to_browse_code(project, commit) if current_controller?(:projects, :commits) if @repo.blob_at(commit.id, @path) - return link_to "Browse File »", project_blob_path(project, tree_join(commit.id, @path)), class: "pull-right" + return link_to( + "Browse File »", + namespace_project_blob_path(project.namespace, project, + tree_join(commit.id, @path)), + class: "pull-right" + ) elsif @path.present? - return link_to "Browse Dir »", project_tree_path(project, tree_join(commit.id, @path)), class: "pull-right" + return link_to( + "Browse Dir »", + namespace_project_tree_path(project.namespace, project, + tree_join(commit.id, @path)), + class: "pull-right" + ) end end - link_to "Browse Code »", project_tree_path(project, commit), class: "pull-right" + link_to( + "Browse Code »", + namespace_project_tree_path(project.namespace, project, commit), + class: "pull-right" + ) end protected @@ -87,19 +134,21 @@ module CommitsHelper # avatar: true will prepend the avatar image # size: size of the avatar image in px def commit_person_link(commit, options = {}) + user = commit.send(options[:source]) + source_name = clean(commit.send "#{options[:source]}_name".to_sym) source_email = clean(commit.send "#{options[:source]}_email".to_sym) - user = User.find_for_commit(source_email, source_name) - person_name = user.nil? ? source_name : user.name - person_email = user.nil? ? source_email : user.email + person_name = user.try(:name) || source_name + person_email = user.try(:email) || source_email - text = if options[:avatar] - avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "") - %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>} - else - person_name - end + text = + if options[:avatar] + avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "") + %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>} + else + person_name + end options = { class: "commit-#{options[:source]}-link has_tooltip", @@ -114,8 +163,11 @@ module CommitsHelper end def view_file_btn(commit_sha, diff, project) - link_to project_blob_path(project, tree_join(commit_sha, diff.new_path)), - class: 'btn btn-small view-file js-view-file' do + link_to( + namespace_project_blob_path(project.namespace, project, + tree_join(commit_sha, diff.new_path)), + class: 'btn btn-small view-file js-view-file' + ) do raw('View file @') + content_tag(:span, commit_sha[0..6], class: 'commit-short-id') end diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index 5ff19b88293..f1dc906cab4 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -1,15 +1,21 @@ module CompareHelper - def compare_to_mr_button? - @project.merge_requests_enabled && - params[:from].present? && - params[:to].present? && - @repository.branch_names.include?(params[:from]) && - @repository.branch_names.include?(params[:to]) && - params[:from] != params[:to] && - !@refs_are_same + def create_mr_button?(from = params[:from], to = params[:to], project = @project) + from.present? && + to.present? && + from != to && + project.merge_requests_enabled && + project.repository.branch_names.include?(from) && + project.repository.branch_names.include?(to) end - def compare_mr_path - new_project_merge_request_path(@project, merge_request: {source_branch: params[:to], target_branch: params[:from]}) + def create_mr_path(from = params[:from], to = params[:to], project = @project) + new_namespace_project_merge_request_path( + project.namespace, + project, + merge_request: { + source_branch: to, + target_branch: from + } + ) end end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index acc0eeb76b3..c25b54eadc6 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -1,67 +1,9 @@ module DashboardHelper - def filter_path(entity, options={}) - exist_opts = { - state: params[:state], - scope: params[:scope], - project_id: params[:project_id], - } - - options = exist_opts.merge(options) - - path = request.path - path << "?#{options.to_param}" - path - end - - def entities_per_project(project, entity) - case entity.to_sym - when :issue then @issues.where(project_id: project.id) - when :merge_request then @merge_requests.where(target_project_id: project.id) - else - [] - end.count - end - - def projects_dashboard_filter_path(options={}) - exist_opts = { - sort: params[:sort], - scope: params[:scope], - group: params[:group], - } - - options = exist_opts.merge(options) - - path = request.path - path << "?#{options.to_param}" - path + def assigned_issues_dashboard_path + issues_dashboard_path(assignee_id: current_user.id) end - def assigned_entities_count(current_user, entity, scope = nil) - items = current_user.send('assigned_' + entity.pluralize) - get_count(items, scope) - end - - def authored_entities_count(current_user, entity, scope = nil) - items = current_user.send(entity.pluralize) - get_count(items, scope) - end - - def authorized_entities_count(current_user, entity, scope = nil) - items = entity.classify.constantize - get_count(items, scope, true, current_user) - end - - protected - - def get_count(items, scope, get_authorized = false, current_user = nil) - items = items.opened - if scope.kind_of?(Group) - items = items.of_group(scope) - elsif scope.kind_of?(Project) - items = items.of_projects(scope) - elsif get_authorized - items = items.of_projects(current_user.authorized_projects) - end - items.count + def assigned_mrs_dashboard_path + merge_requests_dashboard_path(assignee_id: current_user.id) end end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index cb50d89cba8..1bd3ec5e0e0 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -7,14 +7,23 @@ module DiffHelper end end - def safe_diff_files(diffs) - diffs.first(allowed_diff_size).map do |diff| - Gitlab::Diff::File.new(diff) + def allowed_diff_lines + if diff_hard_limit_enabled? + Commit::DIFF_HARD_LIMIT_LINES + else + Commit::DIFF_SAFE_LINES end end - def show_diff_size_warning?(diffs) - diffs.size > allowed_diff_size + def safe_diff_files(diffs) + lines = 0 + safe_files = [] + diffs.first(allowed_diff_size).each do |diff| + lines += diff.diff.lines.count + break if lines > allowed_diff_lines + safe_files << Gitlab::Diff::File.new(diff) + end + safe_files end def diff_hard_limit_enabled? @@ -92,6 +101,10 @@ module DiffHelper (bottom) ? 'js-unfold-bottom' : '' end + def unfold_class(unfold) + (unfold) ? 'unfold js-unfold' : '' + end + def diff_line_content(line) if line.blank? " " @@ -101,7 +114,7 @@ module DiffHelper end def line_comments - @line_comments ||= @line_notes.group_by(&:line_code) + @line_comments ||= @line_notes.select(&:active?).group_by(&:line_code) end def organize_comments(type_left, type_right, line_code_left, line_code_right) @@ -117,4 +130,41 @@ module DiffHelper [comments_left, comments_right] end + + def inline_diff_btn + params_copy = params.dup + params_copy[:view] = 'inline' + # Always use HTML to handle case where JSON diff rendered this button + params_copy.delete(:format) + + link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do + 'Inline' + end + end + + def parallel_diff_btn + params_copy = params.dup + params_copy[:view] = 'parallel' + # Always use HTML to handle case where JSON diff rendered this button + params_copy.delete(:format) + + link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do + 'Side-by-side' + end + end + + def submodule_link(blob, ref, repository = @repository) + tree, commit = submodule_links(blob, ref, repository) + commit_id = if commit.nil? + blob.id[0..10] + else + link_to "#{blob.id[0..10]}", commit + end + + [ + content_tag(:span, link_to(truncate(blob.name, length: 40), tree)), + '@', + content_tag(:span, commit_id, class: 'monospace'), + ].join(' ').html_safe + end end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 24d67c21d6b..128de18bc47 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -29,4 +29,29 @@ module EmailsHelper end end end + + def color_email_diff(diffcontent) + formatter = Rugments::Formatters::HTML.new(cssclass: "highlight", inline_theme: :github) + lexer = Rugments::Lexers::Diff.new + 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 + unit = 'day' + valid_length = (valid_hours / 24).floor + else + unit = 'hour' + valid_length = valid_hours.floor + end + + pluralize(valid_length, unit) + end + + def reset_token_expire_message + link_tag = link_to('request a new one', new_user_password_url(user_email: @user.email)) + msg = "This link is valid for #{password_reset_token_valid_time}. " + msg << "After it expires, you can #{link_tag}." + end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 71f97fbb8c8..d440da050e1 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -10,83 +10,108 @@ module EventsHelper end def event_action_name(event) - target = if event.target_type - event.target_type.titleize.downcase - else - 'project' - end + target = if event.target_type + if event.note? + event.note_target_type + else + event.target_type.titleize.downcase + end + else + 'project' + end [event.action_name, target].join(" ") end def event_filter_link(key, tooltip) key = key.to_s - inactive = if @event_filter.active? key - nil - else - 'inactive' - end - - content_tag :div, class: "filter_icon #{inactive}" do - link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do - content_tag :i, nil, class: icon_for_event[key] + active = 'active' if @event_filter.active?(key) + link_opts = { + class: 'event_filter_link', + id: "#{key}_event_filter", + title: "Filter by #{tooltip.downcase}", + data: { toggle: 'tooltip', placement: 'top' } + } + + content_tag :li, class: "filter_icon #{active}" do + link_to request.path, link_opts do + icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip) end end end def icon_for_event { - EventFilter.push => 'fa fa-upload', - EventFilter.merged => 'fa fa-check-square-o', - EventFilter.comments => 'fa fa-comments', - EventFilter.team => 'fa fa-user', + EventFilter.push => 'upload', + EventFilter.merged => 'check-square-o', + EventFilter.comments => 'comments', + EventFilter.team => 'user', } end def event_feed_title(event) - if event.issue? - "#{event.author_name} #{event.action_name} issue ##{event.target_iid}: #{event.issue_title} at #{event.project_name}" - elsif event.merge_request? - "#{event.author_name} #{event.action_name} MR ##{event.target_iid}: #{event.merge_request_title} at #{event.project_name}" - elsif event.push? - "#{event.author_name} #{event.push_action_name} #{event.ref_type} #{event.ref_name} at #{event.project_name}" - elsif event.membership_changed? - "#{event.author_name} #{event.action_name} #{event.project_name}" - elsif event.note? && event.note_commit? - "#{event.author_name} commented on #{event.note_target_type} #{event.note_short_commit_id} at #{event.project_name}" - elsif event.note? - "#{event.author_name} commented on #{event.note_target_type} ##{truncate event.note_target_iid} at #{event.project_name}" - else - "" + words = [] + words << event.author_name + words << event_action_name(event) + + if event.push? + words << event.ref_type + 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 << "at" + elsif event.target + words << "##{event.target_iid}:" + words << event.target.title if event.target.respond_to?(:title) + words << "at" end + + words << event.project_name + + words.join(" ") end def event_feed_url(event) if event.issue? - project_issue_url(event.project, event.issue) + namespace_project_issue_url(event.project.namespace, event.project, + event.issue) elsif event.merge_request? - project_merge_request_url(event.project, event.merge_request) + namespace_project_merge_request_url(event.project.namespace, + event.project, event.merge_request) elsif event.note? && event.note_commit? - project_commit_url(event.project, event.note_target) + namespace_project_commit_url(event.project.namespace, event.project, + event.note_target) elsif event.note? if event.note_target if event.note_commit? - project_commit_path(event.project, event.note_commit_id, anchor: dom_id(event.target)) + namespace_project_commit_path(event.project.namespace, event.project, + event.note_commit_id, + anchor: dom_id(event.target)) elsif event.note_project_snippet? - project_snippet_path(event.project, event.note_target) + namespace_project_snippet_path(event.project.namespace, + event.project, event.note_target) else event_note_target_path(event) end end elsif event.push? - if event.push_with_commits? + if event.push_with_commits? && event.md_ref? if event.commits_count > 1 - project_compare_url(event.project, from: event.commit_from, to: event.commit_to) + namespace_project_compare_url(event.project.namespace, event.project, + from: event.commit_from, to: + event.commit_to) else - project_commit_url(event.project, id: event.commit_to) + namespace_project_commit_url(event.project.namespace, event.project, + id: event.commit_to) end else - project_commits_url(event.project, event.ref_name) + namespace_project_commits_url(event.project.namespace, event.project, + event.ref_name) end end end @@ -98,8 +123,6 @@ module EventsHelper render "events/event_push", event: event elsif event.merge_request? render "events/event_merge_request", merge_request: event.merge_request - elsif event.push? - render "events/event_push", event: event elsif event.note? render "events/event_note", note: event.note end @@ -107,20 +130,30 @@ module EventsHelper def event_note_target_path(event) if event.note? && event.note_commit? - project_commit_path(event.project, event.note_target) + namespace_project_commit_path(event.project.namespace, event.project, + event.note_target) else - polymorphic_path([event.project, event.note_target], anchor: dom_id(event.target)) + polymorphic_path([event.project.namespace.becomes(Namespace), + event.project, event.note_target], + anchor: dom_id(event.target)) end end def event_note_title_html(event) if event.note_target if event.note_commit? - link_to project_commit_path(event.project, event.note_commit_id, anchor: dom_id(event.target)), class: "commit_short_id" do + link_to( + namespace_project_commit_path(event.project.namespace, event.project, + event.note_commit_id, + anchor: dom_id(event.target)), + class: "commit_short_id" + ) do "#{event.note_target_type} #{event.note_short_commit_id}" end elsif event.note_project_snippet? - link_to(project_snippet_path(event.project, event.note_target)) do + link_to(namespace_project_snippet_path(event.project.namespace, + event.project, + event.note_target)) do "#{event.note_target_type} ##{truncate event.note_target_id}" end else @@ -135,9 +168,9 @@ module EventsHelper end end - def event_note(text) - text = first_line_in_markdown(text, 150) - sanitize(text, tags: %w(a img b pre code p)) + def event_note(text, options = {}) + text = first_line_in_markdown(text, 150, options) + sanitize(text, tags: %w(a img b pre code p span)) end def event_commit_title(message) @@ -145,4 +178,26 @@ module EventsHelper rescue "--broken encoding" end + + def event_to_atom(xml, event) + if event.proper? + 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.strftime("%Y-%m-%dT%H:%M:%S%Z") + xml.media :thumbnail, width: "40", height: "40", 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 end diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb new file mode 100644 index 00000000000..7616fe6bad8 --- /dev/null +++ b/app/helpers/explore_helper.rb @@ -0,0 +1,17 @@ +module ExploreHelper + def explore_projects_filter_path(options={}) + exist_opts = { + sort: params[:sort], + scope: params[:scope], + group: params[:group], + tag: params[:tag], + visibility_level: params[:visibility_level], + } + + options = exist_opts.merge(options) + + path = request.path + path << "?#{options.to_param}" + path + end +end diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb new file mode 100644 index 00000000000..838b85afdfe --- /dev/null +++ b/app/helpers/external_wiki_helper.rb @@ -0,0 +1,11 @@ +module ExternalWikiHelper + def get_project_wiki_path(project) + external_wiki_service = project.services. + select { |service| service.to_param == 'external_wiki' }.first + if external_wiki_service.present? && external_wiki_service.active? + external_wiki_service.properties['external_wiki_url'] + else + namespace_project_wiki_path(project.namespace, project, :home) + end + end +end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 7d3cb749829..9aabe01f60e 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -1,5 +1,8 @@ +require 'nokogiri' + module GitlabMarkdownHelper include Gitlab::Markdown + include PreferencesHelper # Use this in places where you would normally use link_to(gfm(...), ...). # @@ -13,200 +16,88 @@ module GitlabMarkdownHelper def link_to_gfm(body, url, html_options = {}) return "" if body.blank? - escaped_body = if body =~ /^\<img/ + escaped_body = if body =~ /\A\<img/ body else escape_once(body) end - gfm_body = gfm(escaped_body, @project, html_options) + gfm_body = gfm(escaped_body, {}, html_options) - gfm_body.gsub!(%r{<a.*?>.*?</a>}m) do |match| - "</a>#{match}#{link_to("", url, html_options)[0..-5]}" # "</a>".length +1 + fragment = Nokogiri::XML::DocumentFragment.parse(gfm_body) + if fragment.children.size == 1 && fragment.children[0].name == 'a' + # Fragment has only one node, and it's a link generated by `gfm`. + # Replace it with our requested link. + text = fragment.children[0].text + fragment.children[0].replace(link_to(text, url, html_options)) + else + # Traverse the fragment's first generation of children looking for pure + # text, wrapping anything found in the requested link + fragment.children.each do |node| + next unless node.text? + node.replace(link_to(node.text, url, html_options)) + end end - link_to(gfm_body.html_safe, url, html_options) + fragment.to_html.html_safe end + MARKDOWN_OPTIONS = { + no_intra_emphasis: true, + tables: true, + fenced_code_blocks: true, + strikethrough: true, + lax_spacing: true, + space_after_headers: true, + superscript: true, + footnotes: true + }.freeze + def markdown(text, options={}) - unless (@markdown and options == @options) + unless @markdown && options == @options @options = options - gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self, { - # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch- - filter_html: true, - with_toc_data: true, - safe_links_only: true - }.merge(options)) - @markdown = Redcarpet::Markdown.new(gitlab_renderer, - # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use - no_intra_emphasis: true, - tables: true, - fenced_code_blocks: true, - autolink: true, - strikethrough: true, - lax_spacing: true, - space_after_headers: true, - superscript: true) + + # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch + rend = Redcarpet::Render::GitlabHTML.new(self, user_color_scheme_class, options) + + # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use + @markdown = Redcarpet::Markdown.new(rend, MARKDOWN_OPTIONS) end + @markdown.render(text).html_safe end + def asciidoc(text) + Gitlab::Asciidoc.render(text, { + commit: @commit, + project: @project, + project_wiki: @project_wiki, + requested_path: @path, + ref: @ref + }) + end + # Return the first line of +text+, up to +max_chars+, after parsing the line # as Markdown. HTML tags in the parsed output are not counted toward the # +max_chars+ limit. If the length limit falls within a tag's contents, then # the tag contents are truncated without removing the closing tag. - def first_line_in_markdown(text, max_chars = nil) - md = markdown(text).strip + def first_line_in_markdown(text, max_chars = nil, options = {}) + md = markdown(text, options).strip truncate_visible(md, max_chars || md.length) if md.present? end def render_wiki_content(wiki_page) - if wiki_page.format == :markdown + case wiki_page.format + when :markdown markdown(wiki_page.content) + when :asciidoc + asciidoc(wiki_page.content) else wiki_page.formatted_content.html_safe end end - def create_relative_links(text) - paths = extract_paths(text) - - paths.uniq.each do |file_path| - # If project does not have repository - # its nothing to rebuild - # - # TODO: pass project variable to markdown helper instead of using - # instance variable. Right now it generates invalid path for pages out - # of project scope. Example: search results where can be rendered markdown - # from different projects - if @repository && @repository.exists? && !@repository.empty? - new_path = rebuild_path(file_path) - # Finds quoted path so we don't replace other mentions of the string - # eg. "doc/api" will be replaced and "/home/doc/api/text" won't - text.gsub!("\"#{file_path}\"", "\"/#{new_path}\"") - end - end - - text - end - - def extract_paths(text) - links = substitute_links(text) - image_links = substitute_image_links(text) - links + image_links - end - - def substitute_links(text) - links = text.scan(/<a href=\"([^"]*)\">/) - relative_links = links.flatten.reject{ |link| link_to_ignore? link } - relative_links - end - - def substitute_image_links(text) - links = text.scan(/<img src=\"([^"]*)\"/) - relative_links = links.flatten.reject{ |link| link_to_ignore? link } - relative_links - end - - def link_to_ignore?(link) - if link =~ /\#\w+/ - # ignore anchors like <a href="#my-header"> - true - else - ignored_protocols.map{ |protocol| link.include?(protocol) }.any? - end - end - - def ignored_protocols - ["http://","https://", "ftp://", "mailto:"] - end - - def rebuild_path(path) - path.gsub!(/(#.*)/, "") - id = $1 || "" - file_path = relative_file_path(path) - file_path = sanitize_slashes(file_path) - - [ - Gitlab.config.gitlab.relative_url_root, - @project.path_with_namespace, - path_with_ref(file_path), - file_path - ].compact.join("/").gsub(/^\/*|\/*$/, '') + id - end - - def sanitize_slashes(path) - path[0] = "" if path.start_with?("/") - path.chop if path.end_with?("/") - path - end - - def relative_file_path(path) - requested_path = @path - nested_path = build_nested_path(path, requested_path) - return nested_path if file_exists?(nested_path) - path - end - - # Covering a special case, when the link is referencing file in the same directory eg: - # If we are at doc/api/README.md and the README.md contains relative links like [Users](users.md) - # this takes the request path(doc/api/README.md), and replaces the README.md with users.md so the path looks like doc/api/users.md - # If we are at doc/api and the README.md shown in below the tree view - # this takes the request path(doc/api) and adds users.md so the path looks like doc/api/users.md - def build_nested_path(path, request_path) - return request_path if path == "" - return path unless request_path - if local_path(request_path) == "tree" - base = request_path.split("/").push(path) - base.join("/") - else - base = request_path.split("/") - base.pop - base.push(path).join("/") - end - end - - # Checks if the path exists in the repo - # eg. checks if doc/README.md exists, if not then link to blob - def path_with_ref(path) - if file_exists?(path) - "#{local_path(path)}/#{correct_ref}" - else - "blob/#{correct_ref}" - end - end - - def file_exists?(path) - return false if path.nil? - return @repository.blob_at(current_sha, path).present? || @repository.tree(current_sha, path).entries.any? - end - - # Check if the path is pointing to a directory(tree) or a file(blob) - # eg. doc/api is directory and doc/README.md is file - def local_path(path) - return "tree" if @repository.tree(current_sha, path).entries.any? - return "raw" if @repository.blob_at(current_sha, path).image? - return "blob" - end - - def current_sha - if @commit - @commit.id - elsif @repository && !@repository.empty? - if @ref - @repository.commit(@ref).try(:sha) - else - @repository.head_commit.sha - end - end - end - - # We will assume that if no ref exists we can point to master - def correct_ref - @ref ? @ref : "master" - end - private # Return +text+, truncated to +max_chars+ characters, excluding any HTML @@ -254,4 +145,26 @@ module GitlabMarkdownHelper truncated end end + + # Returns the text necessary to reference `entity` across projects + # + # project - Project to reference + # entity - Object that responds to `to_reference` + # + # Examples: + # + # cross_project_reference(project, project.issues.first) + # # => 'namespace1/project1#123' + # + # cross_project_reference(project, project.merge_requests.first) + # # => 'namespace1/project1!345' + # + # Returns a String + def cross_project_reference(project, entity) + if entity.respond_to?(:to_reference) + "#{project.to_reference}#{entity.to_reference}" + else + '' + end + end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb new file mode 100644 index 00000000000..9703c8d9e9c --- /dev/null +++ b/app/helpers/gitlab_routing_helper.rb @@ -0,0 +1,55 @@ +# Shorter routing method for project and project items +# Since update to rails 4.1.9 we are now allowed to use `/` in project routing +# so we use nested routing for project resources which include project and +# project namespace. To avoid writing long methods every time we define shortcuts for +# some of routing. +# +# For example instead of this: +# +# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.projects, merge_request) +# +# We can simply use shortcut: +# +# merge_request_path(merge_request) +# +module GitlabRoutingHelper + def project_path(project, *args) + namespace_project_path(project.namespace, project, *args) + end + + def edit_project_path(project, *args) + edit_namespace_project_path(project.namespace, project, *args) + end + + def issue_path(entity, *args) + namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) + end + + def merge_request_path(entity, *args) + namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args) + end + + def milestone_path(entity, *args) + namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args) + end + + def project_url(project, *args) + namespace_project_url(project.namespace, project, *args) + end + + def edit_project_url(project, *args) + edit_namespace_project_url(project.namespace, project, *args) + end + + def issue_url(entity, *args) + namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args) + end + + def merge_request_url(entity, *args) + namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args) + end + + def project_snippet_url(entity, *args) + namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) + end +end diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index 7cb1b6f8d1a..e1dda20de85 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -1,10 +1,10 @@ module GraphHelper def get_refs(repo, commit) refs = "" - refs += commit.ref_names(repo).join(" ") + refs << commit.ref_names(repo).join(' ') # append note count - refs += "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0 + refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0 refs end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 0dc53dedeb7..3569ac2af63 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,12 +1,16 @@ module GroupsHelper - def remove_user_from_group_message(group, user) - "Are you sure you want to remove \"#{user.name}\" from \"#{group.name}\"?" + def remove_user_from_group_message(group, member) + if member.user + "Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?" + else + "Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?" + end end def leave_group_message(group) "Are you sure you want to leave \"#{group}\" group?" end - + def should_user_see_group_roles?(user, group) if user user.is_admin? || group.members.exists?(user_id: user.id) @@ -15,33 +19,23 @@ module GroupsHelper end end - def group_head_title - title = @group.name - - title = if current_action?(:issues) - "Issues - " + title - elsif current_action?(:merge_requests) - "Merge requests - " + title - elsif current_action?(:members) - "Members - " + title - elsif current_action?(:edit) - "Settings - " + title - else - title - end - - title + def group_settings_page? + if current_controller?('groups') + current_action?('edit') || current_action?('projects') + else + false + end end - def group_filter_path(entity, options={}) - exist_opts = { - status: params[:status] - } - - options = exist_opts.merge(options) + def group_icon(group) + if group.is_a?(String) + group = Group.find_by(path: group) + end - path = request.path - path << "?#{options.to_param}" - path + if group && group.avatar.present? + group.avatar.url + else + image_path('no_group_avatar.png') + end end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index aaa8f8d0077..30b17a736a7 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -1,21 +1,85 @@ module IconsHelper + include FontAwesome::Rails::IconHelper + + # Creates an icon tag given icon name(s) and possible icon modifiers. + # + # Right now this method simply delegates directly to `fa_icon` from the + # font-awesome-rails gem, but should we ever use a different icon pack in the + # future we won't have to change hundreds of method calls. + def icon(names, options = {}) + fa_icon(names, options) + end + + def spinner(text = nil, visible = false) + css_class = 'loading' + css_class << ' hide' unless visible + + content_tag :div, class: css_class do + icon('spinner spin') + text + end + end + def boolean_to_icon(value) if value.to_s == "true" - content_tag :i, nil, class: 'fa fa-circle cgreen' + icon('circle', class: 'cgreen') else - content_tag :i, nil, class: 'fa fa-power-off clgray' + icon('power-off', class: 'clgray') end end def public_icon - content_tag :i, nil, class: 'fa fa-globe' + icon('globe fw') end def internal_icon - content_tag :i, nil, class: 'fa fa-shield' + icon('shield fw') end def private_icon - content_tag :i, nil, class: 'fa fa-lock' + icon('lock fw') + end + + def file_type_icon_class(type, mode, name) + if type == 'folder' + icon_class = 'folder' + elsif mode == '120000' + icon_class = 'share' + else + # Guess which icon to choose based on file extension. + # If you think a file extension is missing, feel free to add it on PR + + case File.extname(name).downcase + when '.pdf' + icon_class = 'file-pdf-o' + when '.jpg', '.jpeg', '.jif', '.jfif', + '.jp2', '.jpx', '.j2k', '.j2c', + '.png', '.gif', '.tif', '.tiff', + '.svg', '.ico', '.bmp' + icon_class = 'file-image-o' + when '.zip', '.zipx', '.tar', '.gz', '.bz', '.bzip', + '.xz', '.rar', '.7z' + icon_class = 'file-archive-o' + when '.mp3', '.wma', '.ogg', '.oga', '.wav', '.flac', '.aac' + icon_class = 'file-audio-o' + when '.mp4', '.m4p', '.m4v', + '.mpg', '.mp2', '.mpeg', '.mpe', '.mpv', + '.mpg', '.mpeg', '.m2v', + '.avi', '.mkv', '.flv', '.ogv', '.mov', + '.3gp', '.3g2' + icon_class = 'file-video-o' + when '.doc', '.dot', '.docx', '.docm', '.dotx', '.dotm', '.docb' + icon_class = 'file-word-o' + when '.xls', '.xlt', '.xlm', '.xlsx', '.xlsm', '.xltx', '.xltm', + '.xlsb', '.xla', '.xlam', '.xll', '.xlw' + icon_class = 'file-excel-o' + when '.ppt', '.pot', '.pps', '.pptx', '.pptm', '.potx', '.potm', + '.ppam', '.ppsx', '.ppsm', '.sldx', '.sldm' + icon_class = 'file-powerpoint-o' + else + icon_class = 'file-text-o' + end + end + + icon_class end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index d513e0ba58e..d4c345fe431 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -13,90 +13,57 @@ module IssuesHelper OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned') end - def url_for_project_issues(project = @project) + def url_for_project_issues(project = @project, options = {}) return '' if project.nil? - if project.used_default_issues_tracker? || !external_issues_tracker_enabled? - project_issues_path(project) + if options[:only_path] + project.issues_tracker.project_path else - url = Gitlab.config.issues_tracker[project.issues_tracker]['project_url'] - url.gsub(':project_id', project.id.to_s). - gsub(':issues_tracker_id', project.issues_tracker_id.to_s) + project.issues_tracker.project_url end end - def url_for_new_issue(project = @project) + def url_for_new_issue(project = @project, options = {}) return '' if project.nil? - if project.used_default_issues_tracker? || !external_issues_tracker_enabled? - url = new_project_issue_path project_id: project + if options[:only_path] + project.issues_tracker.new_issue_path else - issues_tracker = Gitlab.config.issues_tracker[project.issues_tracker] - url = issues_tracker['new_issue_url'] - url.gsub(':project_id', project.id.to_s). - gsub(':issues_tracker_id', project.issues_tracker_id.to_s) + project.issues_tracker.new_issue_url end end - def url_for_issue(issue_iid, project = @project) + def url_for_issue(issue_iid, project = @project, options = {}) return '' if project.nil? - if project.used_default_issues_tracker? || !external_issues_tracker_enabled? - url = project_issue_url project_id: project, id: issue_iid + if options[:only_path] + project.issues_tracker.issue_path(issue_iid) else - url = Gitlab.config.issues_tracker[project.issues_tracker]['issues_url'] - url.gsub(':id', issue_iid.to_s). - gsub(':project_id', project.id.to_s). - gsub(':issues_tracker_id', project.issues_tracker_id.to_s) + project.issues_tracker.issue_url(issue_iid) end end - def title_for_issue(issue_iid, project = @project) - return '' if project.nil? - - if project.used_default_issues_tracker? - issue = project.issues.where(iid: issue_iid).first - return issue.title if issue - end - - '' - end - def issue_timestamp(issue) # Shows the created at time and the updated at time if different - ts = "#{time_ago_with_tooltip(issue.created_at, 'bottom', 'note_created_ago')}" + ts = time_ago_with_tooltip(issue.created_at, placement: 'bottom', html_class: 'note_created_ago') if issue.updated_at != issue.created_at ts << capture_haml do - haml_tag :small do - haml_concat " (Edited #{time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_edited_ago')})" + haml_tag :span do + haml_concat '·' + haml_concat icon('edit', title: 'edited') + haml_concat time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago') end end end ts.html_safe end - # Checks if issues_tracker setting exists in gitlab.yml - def external_issues_tracker_enabled? - Gitlab.config.issues_tracker && Gitlab.config.issues_tracker.values.any? - end - def bulk_update_milestone_options - options_for_select(['None (backlog)']) + + options_for_select([['None (backlog)', -1]]) + options_from_collection_for_select(project_active_milestones, 'id', 'title', params[:milestone_id]) end - def bulk_update_assignee_options(project = @project) - options_for_select(['None (unassigned)']) + - options_from_collection_for_select(project.team.members, 'id', - 'name', params[:assignee_id]) - end - - def assignee_options(object, project = @project) - options_from_collection_for_select(project.team.members.sort_by(&:name), - 'id', 'name', object.assignee_id) - end - def milestone_options(object) options_from_collection_for_select(object.project.milestones.active, 'id', 'title', object.milestone_id) @@ -113,4 +80,24 @@ module IssuesHelper 'issue-box-open' end 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.strftime("%Y-%m-%dT%H:%M:%SZ") + xml.media :thumbnail, width: "40", height: "40", 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 + + # Required for Gitlab::Markdown::IssueReferenceFilter + module_function :url_for_issue end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 19d688c4bb8..8036303851b 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -1,4 +1,44 @@ module LabelsHelper + include ActionView::Helpers::TagHelper + + # Link to a Label + # + # label - Label object to link to + # project - Project object which will be used as the context for the label's + # link. If omitted, defaults to `@project`, or the label's own + # project. + # block - An optional block that will be passed to `link_to`, forming the + # body of the link element. If omitted, defaults to + # `render_colored_label`. + # + # Examples: + # + # # Allow the generated link to use the label's own project + # link_to_label(label) + # + # # Force the generated link to use @project + # @project = Project.first + # link_to_label(label) + # + # # Force the generated link to use a provided project + # link_to_label(label, project: Project.last) + # + # # Customize link body with a block + # link_to_label(label) { "My Custom Label Text" } + # + # Returns a String + def link_to_label(label, project: nil, &block) + project ||= @project || label.project + link = namespace_project_issues_path(project.namespace, project, + label_name: label.name) + + if block_given? + link_to link, &block + else + link_to render_colored_label(label), link + end + end + def project_label_names @project.labels.pluck(:title) end @@ -7,21 +47,38 @@ module LabelsHelper label_color = label.color || Label::DEFAULT_COLOR text_color = text_color_for_bg(label_color) - content_tag :span, class: 'label color-label', style: "background:#{label_color};color:#{text_color}" do - label.name - end + # Intentionally not using content_tag here so that this method can be called + # by LabelReferenceFilter + span = %(<span class="label color-label") + + %( style="background-color: #{label_color}; color: #{text_color}">) + + escape_once(label.name) + '</span>' + + span.html_safe end def suggested_colors [ - '#D9534F', - '#F0AD4E', + '#0033CC', '#428BCA', + '#44AD8E', + '#A8D695', '#5CB85C', + '#69D100', + '#004E00', '#34495E', '#7F8C8D', + '#A295D6', + '#5843AD', '#8E44AD', - '#FFECDB' + '#FFECDB', + '#AD4363', + '#D10069', + '#CC0033', + '#FF0000', + '#D9534F', + '#D1D100', + '#F0AD4E', + '#AD8D43' ] end @@ -29,9 +86,16 @@ module LabelsHelper r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex) if (r + g + b) > 500 - "#333" + '#333333' else - "#FFF" + '#FFFFFF' end end + + def project_labels_options(project) + options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name]) + end + + # Required for Gitlab::Markdown::LabelReferenceFilter + module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index fe6fd5832fc..45ee4fe4135 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -1,25 +1,29 @@ module MergeRequestsHelper def new_mr_path_from_push_event(event) target_project = event.project.forked_from_project || event.project - new_project_merge_request_path( + new_namespace_project_merge_request_path( + event.project.namespace, event.project, new_mr_from_push_event(event, target_project) ) end def new_mr_path_for_fork_from_push_event(event) - new_project_merge_request_path( + new_namespace_project_merge_request_path( + event.project.namespace, event.project, new_mr_from_push_event(event, event.project.forked_from_project) ) end def new_mr_from_push_event(event, target_project) - return :merge_request => { - source_project_id: event.project.id, - target_project_id: target_project.id, - source_branch: event.branch_name, - target_branch: target_project.repository.root_ref + { + merge_request: { + source_project_id: event.project.id, + target_project_id: target_project.id, + source_branch: event.branch_name, + target_branch: target_project.repository.root_ref + } } end @@ -31,7 +35,7 @@ module MergeRequestsHelper end def ci_build_details_path(merge_request) - merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha) + merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch) end def merge_path_description(merge_request, separator) @@ -45,4 +49,16 @@ module MergeRequestsHelper def issues_sentence(issues) issues.map { |i| "##{i.iid}" }.to_sentence end + + def mr_change_branches_path(merge_request) + new_namespace_project_merge_request_path( + @project.namespace, @project, + merge_request: { + source_project_id: @merge_request.source_project_id, + target_project_id: @merge_request.target_project_id, + source_branch: @merge_request.source_branch, + target_branch: nil + } + ) + end end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb new file mode 100644 index 00000000000..93e33ebefd8 --- /dev/null +++ b/app/helpers/milestones_helper.rb @@ -0,0 +1,34 @@ +module MilestonesHelper + def milestones_filter_path(opts = {}) + if @project + namespace_project_milestones_path(@project.namespace, @project, opts) + elsif @group + group_milestones_path(@group, opts) + else + dashboard_milestones_path(opts) + end + end + + def milestone_progress_bar(milestone) + options = { + class: 'progress-bar progress-bar-success', + style: "width: #{milestone.percent_complete}%;" + } + + content_tag :div, class: 'progress' do + content_tag :div, nil, options + end + end + + def projects_milestones_options + milestones = + if @project + @project.milestones + else + Milestone.where(project_id: @projects) + end.active + + grouped_milestones = Milestones::GroupService.new(milestones).execute + options_from_collection_for_select(grouped_milestones, 'title', 'title', params[:milestone_title]) + end +end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index 2bcfde62830..b3132a1f3ba 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -28,7 +28,7 @@ module NamespacesHelper def namespace_icon(namespace, size = 40) if namespace.kind_of?(Group) - group_icon(namespace.path) + group_icon(namespace) else avatar_icon(namespace.owner.email, size) end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb new file mode 100644 index 00000000000..9b1dd8b8e54 --- /dev/null +++ b/app/helpers/nav_helper.rb @@ -0,0 +1,21 @@ +module NavHelper + def nav_menu_collapsed? + cookies[:collapsed_nav] == 'true' + end + + def nav_sidebar_class + if nav_menu_collapsed? + "page-sidebar-collapsed" + else + "page-sidebar-expanded" + end + end + + def nav_header_class + if nav_menu_collapsed? + "header-collapsed" + else + "header-expanded" + end + end +end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 901052edec6..dda9b17d61d 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -1,27 +1,37 @@ module NotesHelper - # Helps to distinguish e.g. commit notes in mr notes list + # 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?) end - def note_target_fields - hidden_field_tag(:target_type, @target_type) + - hidden_field_tag(:target_id, @target_id) + def note_target_fields(note) + hidden_field_tag(:target_type, note.noteable.class.name.underscore) + + hidden_field_tag(:target_id, note.noteable.id) + end + + def note_editable?(note) + 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}", project_commit_path(@project, note.noteable, anchor: note.line_code) + 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 note_timestamp(note) # Shows the created at time and the updated at time if different - ts = "#{time_ago_with_tooltip(note.created_at, 'bottom', 'note_created_ago')}" + ts = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago') if note.updated_at != note.created_at ts << capture_haml do - haml_tag :small do - haml_concat " (Edited #{time_ago_with_tooltip(note.updated_at, 'bottom', 'note_edited_ago')})" + haml_tag :span do + haml_concat '·' + haml_concat icon('edit', title: 'edited') + haml_concat time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago') end end end @@ -37,7 +47,7 @@ module NotesHelper }.to_json end - def link_to_new_diff_note(line_code) + def link_to_new_diff_note(line_code, line_type = nil) discussion_id = Note.build_discussion_id( @comments_target[:noteable_type], @comments_target[:noteable_id] || @comments_target[:commit_id], @@ -49,14 +59,18 @@ module NotesHelper noteable_id: @comments_target[:noteable_id], commit_id: @comments_target[:commit_id], line_code: line_code, - discussion_id: discussion_id + discussion_id: discussion_id, + line_type: line_type } - button_tag '', class: 'btn add-diff-note js-add-diff-note-button', - data: data, title: 'Add a comment to this line' + button_tag(class: 'btn add-diff-note js-add-diff-note-button', + data: data, + title: 'Add a comment to this line') do + icon('comment-o') + end end - def link_to_reply_diff(note) + def link_to_reply_diff(note, line_type = nil) return unless current_user data = { @@ -64,12 +78,13 @@ module NotesHelper noteable_id: note.noteable_id, commit_id: note.commit_id, line_code: note.line_code, - discussion_id: note.discussion_id + discussion_id: note.discussion_id, + line_type: line_type } button_tag class: 'btn reply-btn js-discussion-reply-button', data: data, title: 'Add a reply' do - link_text = content_tag(:i, nil, class: 'fa fa-comment') + link_text = icon('comment') link_text << ' Reply' end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index bad380e98a8..2f8e64c375f 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,13 +1,15 @@ module NotificationsHelper + include IconsHelper + def notification_icon(notification) if notification.disabled? - content_tag :i, nil, class: 'fa fa-volume-off ns-mute' + icon('volume-off', class: 'ns-mute') elsif notification.participating? - content_tag :i, nil, class: 'fa fa-volume-down ns-part' + icon('volume-down', class: 'ns-part') elsif notification.watch? - content_tag :i, nil, class: 'fa fa-volume-up ns-watch' + icon('volume-up', class: 'ns-watch') else - content_tag :i, nil, class: 'fa fa-circle-o ns-default' + icon('circle-o', class: 'ns-default') end end end diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb index 7024483b8b3..997b91de077 100644 --- a/app/helpers/oauth_helper.rb +++ b/app/helpers/oauth_helper.rb @@ -4,7 +4,7 @@ module OauthHelper end def default_providers - [:twitter, :github, :google_oauth2, :ldap] + [:twitter, :github, :gitlab, :bitbucket, :google_oauth2, :ldap] end def enabled_oauth_providers @@ -13,7 +13,22 @@ module OauthHelper def enabled_social_providers enabled_oauth_providers.select do |name| - [:twitter, :github, :google_oauth2].include?(name.to_sym) + [:twitter, :gitlab, :github, :bitbucket, :google_oauth2].include?(name.to_sym) end end + + def additional_providers + enabled_oauth_providers.reject{|provider| provider.to_s.starts_with?('ldap')} + end + + def oauth_image_tag(provider, size = 64) + file_name = "#{provider.to_s.split('_').first}_#{size}.png" + image_tag(image_path("authbuttons/#{file_name}"), alt: "Sign in with #{provider.to_s.titleize}") + end + + def oauth_active?(provider) + current_user.identities.exists?(provider: provider.to_s) + end + + extend self end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb new file mode 100644 index 00000000000..01b6a63552c --- /dev/null +++ b/app/helpers/page_layout_helper.rb @@ -0,0 +1,26 @@ +module PageLayoutHelper + def page_title(*titles) + @page_title ||= [] + + @page_title.push(*titles.compact) if titles.any? + + @page_title.join(" | ") + end + + def header_title(title = nil, title_url = nil) + if title + @header_title = title + @header_title_url = title_url + else + @header_title_url ? link_to(@header_title, @header_title_url) : @header_title + end + end + + def sidebar(name = nil) + if name + @sidebar = name + else + @sidebar + end + end +end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb new file mode 100644 index 00000000000..bceff4fd52e --- /dev/null +++ b/app/helpers/preferences_helper.rb @@ -0,0 +1,53 @@ +# Helper methods for per-User preferences +module PreferencesHelper + COLOR_SCHEMES = { + 1 => 'white', + 2 => 'dark', + 3 => 'solarized-light', + 4 => 'solarized-dark', + 5 => 'monokai', + } + COLOR_SCHEMES.default = 'white' + + # Helper method to access the COLOR_SCHEMES + # + # The keys are the `color_scheme_ids` + # The values are the `name` of the scheme. + # + # The preview images are `name-scheme-preview.png` + # The stylesheets should use the css class `.name` + def color_schemes + COLOR_SCHEMES.freeze + end + + # Maps `dashboard` values to more user-friendly option text + DASHBOARD_CHOICES = { + projects: 'Your Projects (default)', + stars: 'Starred Projects' + }.with_indifferent_access.freeze + + # Returns an Array usable by a select field for more user-friendly option text + def dashboard_choices + defined = User.dashboards + + if defined.size != DASHBOARD_CHOICES.size + # Ensure that anyone adding new options updates this method too + raise RuntimeError, "`User` defines #{defined.size} dashboard choices," + + " but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}." + else + defined.map do |key, _| + # Use `fetch` so `KeyError` gets raised when a key is missing + [DASHBOARD_CHOICES.fetch(key), key] + end + end + end + + def user_application_theme + theme = Gitlab::Themes.by_id(current_user.try(:theme_id)) + theme.css_class + end + + def user_color_scheme_class + COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user) + end +end diff --git a/app/helpers/profile_helper.rb b/app/helpers/profile_helper.rb index 0b375558305..780c7cd5133 100644 --- a/app/helpers/profile_helper.rb +++ b/app/helpers/profile_helper.rb @@ -1,19 +1,13 @@ module ProfileHelper - def oauth_active_class(provider) - if current_user.provider == provider.to_s - 'active' - end - end - def show_profile_username_tab? current_user.can_change_username? end def show_profile_social_tab? - enabled_social_providers.any? && !current_user.ldap_user? + enabled_social_providers.any? end def show_profile_remove_tab? - gitlab_config.signup_enabled && !current_user.ldap_user? + signup_enabled? end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index fb5470d98e5..ec65e473919 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -1,10 +1,14 @@ module ProjectsHelper - def remove_from_project_team_message(project, user) - "You are going to remove #{user.name} from #{project.name} project team. Are you sure?" + def remove_from_project_team_message(project, member) + if member.user + "You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?" + else + "You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?" + end end def link_to_project(project) - link_to project do + link_to [project.namespace.becomes(Namespace), project] do title = content_tag(:span, project.name, class: 'project-name') if project.namespace @@ -42,12 +46,20 @@ module ProjectsHelper def project_title(project) if project.group content_tag :span do - link_to(simple_sanitize(project.group.name), group_path(project.group)) + ' / ' + link_to(simple_sanitize(project.name), project_path(project)) + link_to( + simple_sanitize(project.group.name), group_path(project.group) + ) + ' / ' + + link_to(simple_sanitize(project.name), + project_path(project)) end else owner = project.namespace.owner content_tag :span do - link_to(simple_sanitize(owner.name), user_path(owner)) + ' / ' + link_to(simple_sanitize(project.name), project_path(project)) + link_to( + simple_sanitize(owner.name), user_path(owner) + ) + ' / ' + + link_to(simple_sanitize(project.name), + project_path(project)) end end end @@ -68,76 +80,22 @@ module ProjectsHelper project_nav_tabs.include? name end - def selected_label?(label_name) - params[:label_name].to_s.split(',').include?(label_name) - end - - def labels_filter_path(label_name) - label_name = - if selected_label?(label_name) - params[:label_name].split(',').reject { |l| l == label_name }.join(',') - elsif params[:label_name].present? - "#{params[:label_name]},#{label_name}" - else - label_name - end - - project_filter_path(label_name: label_name) - end - - def label_filter_class(label_name) - if selected_label?(label_name) - 'label-filter-item active' - else - 'label-filter-item light' - end - end - - def project_filter_path(options={}) - exist_opts = { - state: params[:state], - scope: params[:scope], - label_name: params[:label_name], - milestone_id: params[:milestone_id], - assignee_id: params[:assignee_id], - sort: params[:sort], - } - - options = exist_opts.merge(options) - - path = request.path - path << "?#{options.to_param}" - path - end - def project_active_milestones @project.milestones.active.order("due_date, title ASC") end - def project_issues_trackers(current_tracker = nil) - values = Project.issues_tracker.values.map do |tracker_key| - if tracker_key.to_sym == :gitlab - ['GitLab', tracker_key] + def link_to_toggle_star(title, starred) + cls = 'star-btn btn btn-sm btn-default' + + toggle_text = + if starred + ' Unstar' else - [Gitlab.config.issues_tracker[tracker_key]['title'] || tracker_key, tracker_key] + ' Star' end - end - - options_for_select(values, current_tracker) - end - - def link_to_toggle_star(title, starred, signed_in) - cls = 'star-btn' - cls += ' disabled' unless signed_in toggle_html = content_tag('span', class: 'toggle') do - toggle_text = if starred - ' Unstar' - else - ' Star' - end - - content_tag('i', ' ', class: 'fa fa-star') + toggle_text + icon('star') + toggle_text end count_html = content_tag('span', class: 'count') do @@ -149,23 +107,36 @@ module ProjectsHelper class: cls, method: :post, remote: true, - data: {type: 'json'} + data: { type: 'json' } } + path = toggle_star_namespace_project_path(@project.namespace, @project) content_tag 'span', class: starred ? 'turn-on' : 'turn-off' do - link_to toggle_star_project_path(@project), link_opts do + link_to(path, link_opts) do toggle_html + ' ' + count_html end end end def link_to_toggle_fork - out = content_tag(:i, '', class: 'fa fa-code-fork') - out << ' Fork' - out << content_tag(:span, class: 'count') do + html = content_tag('span') do + icon('code-fork') + ' Fork' + end + + count_html = content_tag(:span, class: 'count') do @project.forks_count.to_s end + + html + count_html + end + + def project_for_deploy_key(deploy_key) + if deploy_key.projects.include?(@project) + @project + else + deploy_key.projects.find { |project| can?(current_user, :read_project, project) } + end end private @@ -177,7 +148,7 @@ module ProjectsHelper nav_tabs << [:files, :commits, :network, :graphs] end - if project.repo_exists? && project.merge_requests_enabled + if project.repo_exists? && can?(current_user, :read_merge_request, project) nav_tabs << :merge_requests end @@ -185,8 +156,20 @@ module ProjectsHelper nav_tabs << :settings end - [:issues, :wiki, :snippets].each do |feature| - nav_tabs << feature if project.send :"#{feature}_enabled" + if can?(current_user, :read_issue, project) + nav_tabs << :issues + end + + if can?(current_user, :read_wiki, project) + nav_tabs << :wiki + end + + if can?(current_user, :read_project_snippet, project) + nav_tabs << :snippets + end + + if can?(current_user, :read_milestone, project) + nav_tabs << [:milestones, :labels] end nav_tabs.flatten @@ -217,40 +200,6 @@ module ProjectsHelper 'unknown' end - def project_head_title - title = @project.name_with_namespace - - title = if current_controller?(:tree) - "#{@project.path}\/#{@path} at #{@ref} - " + title - elsif current_controller?(:issues) - if current_action?(:show) - "Issue ##{@issue.iid} - #{@issue.title} - " + title - else - "Issues - " + title - end - elsif current_controller?(:blob) - "#{@project.path}\/#{@blob.path} at #{@ref} - " + title - elsif current_controller?(:commits) - "Commits at #{@ref} - " + title - elsif current_controller?(:merge_requests) - if current_action?(:show) - "Merge request ##{@merge_request.iid} - " + title - else - "Merge requests - " + title - end - elsif current_controller?(:wikis) - "Wiki - " + title - elsif current_controller?(:network) - "Network graph - " + title - elsif current_controller?(:graphs) - "Graphs - " + title - else - title - end - - title - end - def default_url_to_repo(project = nil) project = project || @project current_user ? project.url_to_repo : project.http_url_to_repo @@ -262,21 +211,99 @@ module ProjectsHelper def project_last_activity(project) if project.last_activity_at - time_ago_with_tooltip(project.last_activity_at, 'bottom', 'last_activity_time_ago') + time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago') else "Never" end end def contribution_guide_url(project) - if project && project.repository.contribution_guide - project_blob_path(project, tree_join(project.default_branch, project.repository.contribution_guide.name)) + if project && contribution_guide = project.repository.contribution_guide + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + contribution_guide.name) + ) + end + end + + def changelog_url(project) + if project && changelog = project.repository.changelog + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + changelog.name) + ) + end + end + + def license_url(project) + if project && license = project.repository.license + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + license.name) + ) + end + end + + def version_url(project) + if project && version = project.repository.version + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + version.name) + ) end end def hidden_pass_url(original_url) result = URI(original_url) - result.password = '*****' if result.password.present? + result.password = '*****' unless result.password.nil? result + rescue + original_url + end + + def project_wiki_path_with_version(proj, page, version, is_newest) + url_params = is_newest ? {} : { version_id: version } + namespace_project_wiki_path(proj.namespace, proj, page, url_params) + end + + def project_status_css_class(status) + case status + when "started" + "active" + when "failed" + "danger" + when "finished" + "success" + end + end + + def service_field_value(type, value) + return value unless type == 'password' + + if value.present? + "***********" + else + nil + end + end + + def user_max_access_in_project(user, project) + level = project.team.max_member_access(user) + + if level + Gitlab::Access.options_with_owner.key(level) + end + end + + def leave_project_message(project) + "Are you sure you want to leave \"#{project.name}\" project?" end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 65b9408cfa1..c31a556ff7b 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -23,9 +23,9 @@ module SearchHelper # Autocomplete results for various settings pages def default_autocomplete [ - { label: "My Profile settings", url: profile_path }, - { label: "My SSH Keys", url: profile_keys_path }, - { label: "My Dashboard", url: root_path }, + { label: "Profile settings", url: profile_path }, + { label: "SSH Keys", url: profile_keys_path }, + { label: "Dashboard", url: root_path }, { label: "Admin Section", url: admin_root_path }, ] end @@ -52,16 +52,16 @@ module SearchHelper ref = @ref || @project.repository.root_ref [ - { label: "#{prefix} - Files", url: project_tree_path(@project, ref) }, - { label: "#{prefix} - Commits", url: project_commits_path(@project, ref) }, - { label: "#{prefix} - Network", url: project_network_path(@project, ref) }, - { label: "#{prefix} - Graph", url: project_graph_path(@project, ref) }, - { label: "#{prefix} - Issues", url: project_issues_path(@project) }, - { label: "#{prefix} - Merge Requests", url: project_merge_requests_path(@project) }, - { label: "#{prefix} - Milestones", url: project_milestones_path(@project) }, - { label: "#{prefix} - Snippets", url: project_snippets_path(@project) }, - { label: "#{prefix} - Team", url: project_team_index_path(@project) }, - { label: "#{prefix} - Wiki", url: project_wikis_path(@project) }, + { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, + { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, + { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, + { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, + { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) }, + { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, + { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, + { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, + { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else [] @@ -84,7 +84,7 @@ module SearchHelper sorted_by_stars.non_archived.limit(limit).map do |p| { label: "project: #{search_result_sanitize(p.name_with_namespace)}", - url: project_path(p) + url: namespace_project_path(p.namespace, p) } end end diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index ab24367c455..2b99a398049 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -4,17 +4,40 @@ module SelectsHelper css_class << "multiselect " if opts[:multiple] css_class << (opts[:class] || '') value = opts[:selected] || '' + placeholder = opts[:placeholder] || 'Search for a user' - hidden_field_tag(id, value, class: css_class) + null_user = opts[:null_user] || false + any_user = opts[:any_user] || false + email_user = opts[:email_user] || false + first_user = opts[:first_user] && current_user ? current_user.username : false + project = opts[:project] || @project + + html = { + class: css_class, + 'data-placeholder' => placeholder, + 'data-null-user' => null_user, + 'data-any-user' => any_user, + 'data-email-user' => email_user, + 'data-first-user' => first_user + } + + unless opts[:scope] == :all + if project + html['data-project-id'] = project.id + elsif @group + html['data-group-id'] = @group.id + end + end + + hidden_field_tag(id, value, html) end - def project_users_select_tag(id, opts = {}) - css_class = "ajax-project-users-select " + def groups_select_tag(id, opts = {}) + css_class = "ajax-groups-select " css_class << "multiselect " if opts[:multiple] css_class << (opts[:class] || '') value = opts[:selected] || '' - placeholder = opts[:placeholder] || 'Select user' - project_id = opts[:project_id] || @project.id - hidden_field_tag(id, value, class: css_class, 'data-placeholder' => placeholder, 'data-project-id' => project_id) + + hidden_field_tag(id, value, class: css_class) end end diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index b0abc2cae33..906cb12cd48 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -11,7 +11,8 @@ module SnippetsHelper def reliable_snippet_path(snippet) if snippet.project_id? - project_snippet_path(snippet.project, snippet) + namespace_project_snippet_path(snippet.project.namespace, + snippet.project, snippet) else snippet_path(snippet) end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb new file mode 100644 index 00000000000..bb12d43f397 --- /dev/null +++ b/app/helpers/sorting_helper.rb @@ -0,0 +1,96 @@ +module SortingHelper + def sort_options_hash + { + sort_value_name => sort_title_name, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_recently_created => sort_title_recently_created, + sort_value_oldest_created => sort_title_oldest_created, + sort_value_milestone_soon => sort_title_milestone_soon, + sort_value_milestone_later => sort_title_milestone_later, + sort_value_largest_repo => sort_title_largest_repo, + sort_value_recently_signin => sort_title_recently_signin, + sort_value_oldest_signin => sort_title_oldest_signin, + } + end + + def sort_title_oldest_updated + 'Oldest updated' + end + + def sort_title_recently_updated + 'Recently updated' + end + + def sort_title_oldest_created + 'Oldest created' + end + + def sort_title_recently_created + 'Recently created' + end + + def sort_title_milestone_soon + 'Milestone due soon' + end + + def sort_title_milestone_later + 'Milestone due later' + end + + def sort_title_name + 'Name' + end + + def sort_title_largest_repo + 'Largest repository' + end + + def sort_title_recently_signin + 'Recent sign in' + end + + def sort_title_oldest_signin + 'Oldest sign in' + end + + def sort_value_oldest_updated + 'updated_asc' + end + + def sort_value_recently_updated + 'updated_desc' + end + + def sort_value_oldest_created + 'created_asc' + end + + def sort_value_recently_created + 'created_desc' + end + + def sort_value_milestone_soon + 'milestone_due_asc' + end + + def sort_value_milestone_later + 'milestone_due_desc' + end + + def sort_value_name + 'name_asc' + end + + def sort_value_largest_repo + 'repository_size_desc' + end + + def sort_value_recently_signin + 'recent_sign_in' + end + + def sort_value_oldest_signin + 'oldest_sign_in' + end +end diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index 09e5c08e621..6def7793dc3 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -2,22 +2,25 @@ module SubmoduleHelper include Gitlab::ShellAdapter # links to files listing for submodule if submodule is a project on this server - def submodule_links(submodule_item) - url = @repository.submodule_url_for(@ref, submodule_item.path) + def submodule_links(submodule_item, ref = nil, repository = @repository) + url = repository.submodule_url_for(ref, submodule_item.path) - return url, nil unless url =~ /([^\/:]+\/[^\/]+\.git)\Z/ + return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/ - project = $1 + namespace = $1 + project = $2 project.chomp!('.git') - if self_url?(url, project) - return project_path(project), project_tree_path(project, submodule_item.id) + if self_url?(url, namespace, project) + return namespace_project_path(namespace, project), + namespace_project_tree_path(namespace, project, + submodule_item.id) elsif relative_self_url?(url) relative_self_links(url, submodule_item.id) elsif github_dot_com_url?(url) - standard_links('github.com', project, submodule_item.id) + standard_links('github.com', namespace, project, submodule_item.id) elsif gitlab_dot_com_url?(url) - standard_links('gitlab.com', project, submodule_item.id) + standard_links('gitlab.com', namespace, project, submodule_item.id) else return url, nil end @@ -33,27 +36,39 @@ module SubmoduleHelper url =~ /gitlab\.com[\/:][^\/]+\/[^\/]+\Z/ end - def self_url?(url, project) - return true if url == [ Gitlab.config.gitlab.url, '/', project, '.git' ].join('') - url == gitlab_shell.url_to_repo(project) + def self_url?(url, namespace, project) + return true if url == [ Gitlab.config.gitlab.url, '/', namespace, '/', + project, '.git' ].join('') + url == gitlab_shell.url_to_repo([namespace, '/', project].join('')) end def relative_self_url?(url) # (./)?(../repo.git) || (./)?(../../project/repo.git) ) - url =~ /^((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\Z/ || url =~ /^((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\Z/ + url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/ end - def standard_links(host, project, commit) - base = [ 'https://', host, '/', project ].join('') - return base, [ base, '/tree/', commit ].join('') + def standard_links(host, namespace, project, commit) + base = [ 'https://', host, '/', namespace, '/', project ].join('') + [base, [ base, '/tree/', commit ].join('')] end def relative_self_links(url, commit) - if url.scan(/(\.\.\/)/).size == 2 - base = url[/([^\/]*\/[^\/]*)\.git/, 1] - else - base = [ @project.group.path, '/', url[/([^\/]*)\.git/, 1] ].join('') + # Map relative links to a namespace and project + # For example: + # ../bar.git -> same namespace, repo bar + # ../foo/bar.git -> namespace foo, repo bar + # ../../foo/bar/baz.git -> namespace bar, repo baz + components = url.split('/') + base = components.pop.gsub(/.git$/, '') + namespace = components.pop.gsub(/^\.\.$/, '') + + if namespace.empty? + namespace = @project.namespace.name end - return project_path(base), project_tree_path(base, commit) + + [ + namespace_project_path(namespace, base), + namespace_project_tree_path(namespace, base, commit) + ] end end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index bc43e078568..77727337f07 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -28,6 +28,10 @@ module TabHelper # nav_link(controller: [:tree, :refs]) { "Hello" } # # => '<li class="active">Hello</li>' # + # # Several paths + # nav_link(path: ['tree#show', 'profile#show']) { "Hello" } + # # => '<li class="active">Hello</li>' + # # # Shorthand path # nav_link(path: 'tree#show') { "Hello" } # # => '<li class="active">Hello</li>' @@ -38,25 +42,7 @@ module TabHelper # # Returns a list item element String def nav_link(options = {}, &block) - if path = options.delete(:path) - if path.respond_to?(:each) - c = path.map { |p| p.split('#').first } - a = path.map { |p| p.split('#').last } - else - c, a, _ = path.split('#') - end - else - c = options.delete(:controller) - a = options.delete(:action) - end - - if c && a - # When given both options, make sure BOTH are active - klass = current_controller?(*c) && current_action?(*a) ? 'active' : '' - else - # Otherwise check EITHER option - klass = current_controller?(*c) || current_action?(*a) ? 'active' : '' - end + klass = active_nav_link?(options) ? 'active' : '' # Add our custom class into the html_options, which may or may not exist # and which may or may not already have a :class key @@ -72,18 +58,47 @@ module TabHelper end end + def active_nav_link?(options) + if path = options.delete(:path) + unless path.respond_to?(:each) + path = [path] + end + + path.any? do |single_path| + current_path?(single_path) + end + else + c = options.delete(:controller) + a = options.delete(:action) + + if c && a + # When given both options, make sure BOTH are true + current_controller?(*c) && current_action?(*a) + else + # Otherwise check EITHER option + current_controller?(*c) || current_action?(*a) + end + end + end + + def current_path?(path) + c, a, _ = path.split('#') + current_controller?(c) && current_action?(a) + end + def project_tab_class return "active" if current_page?(controller: "/projects", action: :edit, id: @project) - if ['services', 'hooks', 'deploy_keys', 'team_members', 'protected_branches'].include? controller.controller_name - "active" + if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name + "active" end end def branches_tab_class if current_controller?(:protected_branches) || current_controller?(:branches) || - current_page?(project_repository_path(@project)) + current_page?(namespace_project_repository_path(@project.namespace, + @project)) 'active' end end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index ef89bb32c6d..fb85544df2d 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -6,7 +6,7 @@ module TagsHelper def tag_list(project) html = '' project.tag_list.each do |tag| - html += link_to tag, tag_path(tag) + html << link_to(tag, tag_path(tag)) end html.html_safe diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 8e209498323..03a49e119b8 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -10,38 +10,31 @@ module TreeHelper tree = "" # Render folders if we have any - tree += render partial: 'projects/tree/tree_item', collection: folders, locals: {type: 'folder'} if folders.present? + tree << render(partial: 'projects/tree/tree_item', collection: folders, + locals: { type: 'folder' }) if folders.present? # Render files if we have any - tree += render partial: 'projects/tree/blob_item', collection: files, locals: {type: 'file'} if files.present? + tree << render(partial: 'projects/tree/blob_item', collection: files, + locals: { type: 'file' }) if files.present? # Render submodules if we have any - tree += render partial: 'projects/tree/submodule_item', collection: submodules if submodules.present? + tree << render(partial: 'projects/tree/submodule_item', + collection: submodules) if submodules.present? tree.html_safe end def render_readme(readme) - if gitlab_markdown?(readme.name) - preserve(markdown(readme.data)) - elsif markup?(readme.name) - render_markup(readme.name, readme.data) - else - simple_format(readme.data) - end + render_markup(readme.name, readme.data) end - # Return an image icon depending on the file type + # Return an image icon depending on the file type and mode # # type - String type of the tree item; either 'folder' or 'file' - def tree_icon(type) - icon_class = if type == 'folder' - 'fa fa-folder' - else - 'fa fa-file-o' - end - - content_tag :i, nil, class: icon_class + # mode - File unix mode + # name - File name + def tree_icon(type, mode, name) + icon("#{file_type_icon_class(type, mode, name)} fw") end def tree_hex_class(content) @@ -53,14 +46,12 @@ module TreeHelper File.join(*args) end - def allowed_tree_edit? - return false unless @repository.branch_names.include?(@ref) + def allowed_tree_edit?(project = nil, ref = nil) + project ||= @project + ref ||= @ref + return false unless project.repository.branch_names.include?(ref) - if @project.protected_branch? @ref - can?(current_user, :push_code_to_protected_branches, @project) - else - can?(current_user, :push_code, @project) - end + ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) end def tree_breadcrumbs(tree, max_links = 2) @@ -80,20 +71,18 @@ module TreeHelper end end - def up_dir_path(tree) + def up_dir_path file = File.join(@path, "..") tree_join(@ref, file) end - def leave_edit_message - "Leave edit mode?\nAll unsaved changes will be lost." - end - - def editing_preview_title(filename) - if Gitlab::MarkdownHelper.previewable?(filename) - 'Preview' + # returns the relative path of the first subdir that doesn't have only one directory descendant + def flatten_tree(tree) + subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path) + if subtree.count == 1 && subtree.first.dir? + return tree_join(tree.name, flatten_tree(subtree.first)) else - 'Diff' + return tree.name end end end diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb new file mode 100644 index 00000000000..f64d730b448 --- /dev/null +++ b/app/helpers/version_check_helper.rb @@ -0,0 +1,7 @@ +module VersionCheckHelper + def version_status_badge + if Rails.env.production? + image_tag VersionCheck.new.url + end + end +end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index deb9c8b4d49..00d4c7f1051 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -10,7 +10,21 @@ module VisibilityLevelHelper end end - def visibility_level_description(level) + # Return the description for the +level+ argument. + # + # +level+ One of the Gitlab::VisibilityLevel constants + # +form_model+ Either a model object (Project, Snippet, etc.) or the name of + # a Project or Snippet class. + def visibility_level_description(level, form_model) + case form_model.is_a?(String) ? form_model : form_model.class.name + when 'PersonalSnippet', 'ProjectSnippet', 'Snippet' + snippet_visibility_level_description(level) + when 'Project' + project_visibility_level_description(level) + end + end + + def project_visibility_level_description(level) capture_haml do haml_tag :span do case level @@ -33,7 +47,7 @@ module VisibilityLevelHelper haml_tag :span do case level when Gitlab::VisibilityLevel::PRIVATE - haml_concat "The snippet is visible only for me" + haml_concat "The snippet is visible only for me." when Gitlab::VisibilityLevel::INTERNAL haml_concat "The snippet is visible for any logged in user." when Gitlab::VisibilityLevel::PUBLIC @@ -60,7 +74,16 @@ module VisibilityLevelHelper Project.visibility_levels.key(level) end - def restricted_visibility_levels - current_user.is_admin? ? [] : gitlab_config.restricted_visibility_levels + def restricted_visibility_levels(show_all = false) + return [] if current_user.is_admin? && !show_all + current_application_settings.restricted_visibility_levels || [] + end + + def default_project_visibility + current_application_settings.default_project_visibility + end + + def default_snippet_visibility + current_application_settings.default_snippet_visibility end end diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb new file mode 100644 index 00000000000..f8a96516e61 --- /dev/null +++ b/app/helpers/wiki_helper.rb @@ -0,0 +1,24 @@ +module WikiHelper + # Rails v4.1.9+ escapes all model IDs, converting slashes into %2F. The + # only way around this is to implement our own path generators. + def namespace_project_wiki_path(namespace, project, wiki_page, *args) + slug = + case wiki_page + when Symbol + wiki_page + when String + wiki_page + else + wiki_page.slug + end + namespace_project_path(namespace, project) + "/wikis/#{slug}" + end + + def edit_namespace_project_wiki_path(namespace, project, wiki_page, *args) + namespace_project_wiki_path(namespace, project, wiki_page) + '/edit' + end + + def history_namespace_project_wiki_path(namespace, project, wiki_page, *args) + namespace_project_wiki_path(namespace, project, wiki_page) + '/history' + end +end diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb new file mode 100644 index 00000000000..b616add283a --- /dev/null +++ b/app/mailers/devise_mailer.rb @@ -0,0 +1,4 @@ +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 +end diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb index 8c09389985e..1c43f95dc8c 100644 --- a/app/mailers/emails/groups.rb +++ b/app/mailers/emails/groups.rb @@ -1,11 +1,52 @@ module Emails module Groups - def group_access_granted_email(user_group_id) - @membership = GroupMember.find(user_group_id) - @group = @membership.group + def group_access_granted_email(group_member_id) + @group_member = GroupMember.find(group_member_id) + @group = @group_member.group + @target_url = group_url(@group) - mail(to: @membership.user.email, + @current_user = @group_member.user + + mail(to: @group_member.user.notification_email, subject: subject("Access to group was granted")) end + + def group_member_invited_email(group_member_id, token) + @group_member = GroupMember.find group_member_id + @group = @group_member.group + @token = token + + @target_url = group_url(@group) + @current_user = @group_member.user + + mail(to: @group_member.invite_email, + subject: "Invitation to join group #{@group.name}") + end + + def group_invite_accepted_email(group_member_id) + @group_member = GroupMember.find group_member_id + return if @group_member.created_by.nil? + + @group = @group_member.group + + @target_url = group_url(@group) + @current_user = @group_member.created_by + + mail(to: @group_member.created_by.notification_email, + subject: subject("Invitation accepted")) + end + + def group_invite_declined_email(group_id, invite_email, access_level, created_by_id) + return if created_by_id.nil? + + @group = Group.find(group_id) + @current_user = @created_by = User.find(created_by_id) + @access_level = access_level + @invite_email = invite_email + + @target_url = group_url(@group) + mail(to: @created_by.notification_email, + subject: subject("Invitation declined")) + end end end diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index e5346235963..687bac3aa31 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -3,7 +3,7 @@ module Emails def new_issue_email(recipient_id, issue_id) @issue = Issue.find(issue_id) @project = @issue.project - @target_url = project_issue_url(@project, @issue) + @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) mail_new_thread(@issue, from: sender(@issue.author_id), to: recipient(recipient_id), @@ -14,7 +14,7 @@ module Emails @issue = Issue.find(issue_id) @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id @project = @issue.project - @target_url = project_issue_url(@project, @issue) + @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) mail_answer_thread(@issue, from: sender(updated_by_user_id), to: recipient(recipient_id), @@ -25,7 +25,7 @@ module Emails @issue = Issue.find issue_id @project = @issue.project @updated_by = User.find updated_by_user_id - @target_url = project_issue_url(@project, @issue) + @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) mail_answer_thread(@issue, from: sender(updated_by_user_id), to: recipient(recipient_id), @@ -37,7 +37,7 @@ module Emails @issue_status = status @project = @issue.project @updated_by = User.find updated_by_user_id - @target_url = project_issue_url(@project, @issue) + @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) mail_answer_thread(@issue, from: sender(updated_by_user_id), to: recipient(recipient_id), diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 9ecdac87d72..512a8f7ea6b 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -3,7 +3,9 @@ module Emails def new_merge_request_email(recipient_id, merge_request_id) @merge_request = MergeRequest.find(merge_request_id) @project = @merge_request.project - @target_url = project_merge_request_url(@project, @merge_request) + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request) mail_new_thread(@merge_request, from: sender(@merge_request.author_id), to: recipient(recipient_id), @@ -14,7 +16,9 @@ module Emails @merge_request = MergeRequest.find(merge_request_id) @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id @project = @merge_request.project - @target_url = project_merge_request_url(@project, @merge_request) + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request) mail_answer_thread(@merge_request, from: sender(updated_by_user_id), to: recipient(recipient_id), @@ -25,7 +29,9 @@ module Emails @merge_request = MergeRequest.find(merge_request_id) @updated_by = User.find updated_by_user_id @project = @merge_request.project - @target_url = project_merge_request_url(@project, @merge_request) + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request) mail_answer_thread(@merge_request, from: sender(updated_by_user_id), to: recipient(recipient_id), @@ -35,7 +41,9 @@ module Emails def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) @merge_request = MergeRequest.find(merge_request_id) @project = @merge_request.project - @target_url = project_merge_request_url(@project, @merge_request) + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request) mail_answer_thread(@merge_request, from: sender(updated_by_user_id), to: recipient(recipient_id), @@ -47,7 +55,9 @@ module Emails @mr_status = status @project = @merge_request.project @updated_by = User.find updated_by_user_id - @target_url = project_merge_request_url(@project, @merge_request) + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request) set_reference("merge_request_#{merge_request_id}") mail_answer_thread(@merge_request, from: sender(updated_by_user_id), @@ -56,7 +66,7 @@ module Emails end end - # Over rides default behavour to show source/target + # Over rides default behaviour to show source/target # Formats arguments into a String suitable for use as an email subject # # extra - Extra Strings to be inserted into the subject diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index ef9af726a6c..ff251209e01 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -4,7 +4,9 @@ module Emails @note = Note.find(note_id) @commit = @note.noteable @project = @note.project - @target_url = project_commit_url(@project, @commit, anchor: "note_#{@note.id}") + @target_url = namespace_project_commit_url(@project.namespace, @project, + @commit, anchor: + "note_#{@note.id}") mail_answer_thread(@commit, from: sender(@note.author_id), to: recipient(recipient_id), @@ -15,7 +17,9 @@ module Emails @note = Note.find(note_id) @issue = @note.noteable @project = @note.project - @target_url = project_issue_url(@project, @issue, anchor: "note_#{@note.id}") + @target_url = namespace_project_issue_url(@project.namespace, @project, + @issue, anchor: + "note_#{@note.id}") mail_answer_thread(@issue, from: sender(@note.author_id), to: recipient(recipient_id), @@ -26,7 +30,10 @@ module Emails @note = Note.find(note_id) @merge_request = @note.noteable @project = @note.project - @target_url = project_merge_request_url(@project, @merge_request, anchor: "note_#{@note.id}") + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request, anchor: + "note_#{@note.id}") mail_answer_thread(@merge_request, from: sender(@note.author_id), to: recipient(recipient_id), diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index 6d7f8eb4b02..3a83b083109 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -1,23 +1,23 @@ module Emails module Profile def new_user_email(user_id, token = nil) - @user = User.find(user_id) + @current_user = @user = User.find(user_id) @target_url = user_url(@user) @token = token - mail(to: @user.email, subject: subject("Account was created for you")) + mail(to: @user.notification_email, subject: subject("Account was created for you")) end def new_email_email(email_id) @email = Email.find(email_id) - @user = @email.user - mail(to: @user.email, subject: subject("Email was added to your account")) + @current_user = @user = @email.user + mail(to: @user.notification_email, subject: subject("Email was added to your account")) end def new_ssh_key_email(key_id) @key = Key.find(key_id) - @user = @key.user + @current_user = @user = @key.user @target_url = user_url(@user) - mail(to: @user.email, subject: subject("SSH key was added to your account")) + mail(to: @user.notification_email, subject: subject("SSH key was added to your account")) end end end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index d6edfd7059f..4a6e18e6a74 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -1,39 +1,142 @@ module Emails module Projects - def project_access_granted_email(user_project_id) - @project_member = ProjectMember.find user_project_id + def project_access_granted_email(project_member_id) + @project_member = ProjectMember.find project_member_id @project = @project_member.project - @target_url = project_url(@project) - mail(to: @project_member.user.email, + + @target_url = namespace_project_url(@project.namespace, @project) + @current_user = @project_member.user + + mail(to: @project_member.user.notification_email, subject: subject("Access to project was granted")) end + def project_member_invited_email(project_member_id, token) + @project_member = ProjectMember.find project_member_id + @project = @project_member.project + @token = token + + @target_url = namespace_project_url(@project.namespace, @project) + @current_user = @project_member.user + + mail(to: @project_member.invite_email, + subject: "Invitation to join project #{@project.name_with_namespace}") + end + + def project_invite_accepted_email(project_member_id) + @project_member = ProjectMember.find project_member_id + return if @project_member.created_by.nil? + + @project = @project_member.project + + @target_url = namespace_project_url(@project.namespace, @project) + @current_user = @project_member.created_by + + mail(to: @project_member.created_by.notification_email, + subject: subject("Invitation accepted")) + end + + def project_invite_declined_email(project_id, invite_email, access_level, created_by_id) + return if created_by_id.nil? + + @project = Project.find(project_id) + @current_user = @created_by = User.find(created_by_id) + @access_level = access_level + @invite_email = invite_email + + @target_url = namespace_project_url(@project.namespace, @project) + + mail(to: @created_by.notification_email, + subject: subject("Invitation declined")) + end + def project_was_moved_email(project_id, user_id) - @user = User.find user_id + @current_user = @user = User.find user_id @project = Project.find project_id - @target_url = project_url(@project) - mail(to: @user.email, + @target_url = namespace_project_url(@project.namespace, @project) + mail(to: @user.notification_email, subject: subject("Project was moved")) end - def repository_push_email(project_id, recipient, author_id, branch, compare) + def repository_push_email(project_id, recipient, author_id: nil, + ref: nil, + action: nil, + compare: nil, + reverse_compare: false, + send_from_committer_email: false, + disable_diffs: false) + unless author_id && ref && action + raise ArgumentError, "missing keywords: author_id, ref, action" + end + @project = Project.find(project_id) - @author = User.find(author_id) + @current_user = @author = User.find(author_id) + @reverse_compare = reverse_compare @compare = compare - @commits = Commit.decorate(compare.commits) - @diffs = compare.diffs - @branch = branch - if @commits.length > 1 - @target_url = project_compare_url(@project, from: @commits.first, to: @commits.last) - @subject = "#{@commits.length} new commits pushed to repository" + @ref_name = Gitlab::Git.ref_name(ref) + @ref_type = Gitlab::Git.tag_ref?(ref) ? "tag" : "branch" + @action = action + @disable_diffs = disable_diffs + + if @compare + @commits = Commit.decorate(compare.commits, @project) + @diffs = compare.diffs + end + + @action_name = + case action + when :create + "pushed new" + when :delete + "deleted" + else + "pushed to" + end + + @subject = "[Git]" + @subject << "[#{@project.path_with_namespace}]" + @subject << "[#{@ref_name}]" if action == :push + @subject << " " + + if action == :push + if @commits.length > 1 + @target_url = namespace_project_compare_url(@project.namespace, + @project, + from: Commit.new(@compare.base, @project), + to: Commit.new(@compare.head, @project)) + @subject << "Deleted " if @reverse_compare + @subject << "#{@commits.length} commits: #{@commits.first.title}" + else + @target_url = namespace_project_commit_url(@project.namespace, + @project, @commits.first) + + @subject << "Deleted 1 commit: " if @reverse_compare + @subject << @commits.first.title + end else - @target_url = project_commit_url(@project, @commits.first) - @subject = @commits.first.title + unless action == :delete + @target_url = namespace_project_tree_url(@project.namespace, + @project, @ref_name) + end + + subject_action = @action_name.dup + subject_action[0] = subject_action[0].capitalize + @subject << "#{subject_action} #{@ref_type} #{@ref_name}" end - mail(from: sender(author_id), - to: recipient, - subject: subject(@subject)) + @disable_footer = true + + reply_to = + if send_from_committer_email && can_send_from_user_email?(@author) + @author.email + else + Gitlab.config.gitlab.email_reply_to + end + + mail(from: sender(author_id, send_from_committer_email), + reply_to: reply_to, + to: recipient, + subject: @subject) end end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 0ee19836627..79fb48b00d3 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -13,36 +13,66 @@ class Notify < ActionMailer::Base add_template_helper MergeRequestsHelper add_template_helper EmailsHelper - default_url_options[:host] = Gitlab.config.gitlab.host - default_url_options[:protocol] = Gitlab.config.gitlab.protocol - default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port? - default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root + attr_accessor :current_user + helper_method :current_user, :can? default from: Proc.new { default_sender_address.format } - default reply_to: "noreply@#{Gitlab.config.gitlab.host}" + default reply_to: Gitlab.config.gitlab.email_reply_to # Just send email with 2 seconds delay def self.delay delay_for(2.seconds) end + def test_email(recipient_email, subject, body) + mail(to: recipient_email, + subject: subject, + body: body.html_safe, + content_type: 'text/html' + ) + end + + # Splits "gitlab.corp.company.com" up into "gitlab.corp.company.com", + # "corp.company.com" and "company.com". + # Respects set tld length so "company.co.uk" won't match "somethingelse.uk" + def self.allowed_email_domains + domain_parts = Gitlab.config.gitlab.host.split(".") + allowed_domains = [] + begin + allowed_domains << domain_parts.join(".") + domain_parts.shift + end while domain_parts.length > ActionDispatch::Http::URL.tld_length + + allowed_domains + end + private # The default email address to send emails from def default_sender_address address = Mail::Address.new(Gitlab.config.gitlab.email_from) - address.display_name = "GitLab" + address.display_name = Gitlab.config.gitlab.email_display_name address end + def can_send_from_user_email?(sender) + sender_domain = sender.email.split("@").last + self.class.allowed_email_domains.include?(sender_domain) + end + # Return an email address that displays the name of the sender. # Only the displayed name changes; the actual email address is always the same. - def sender(sender_id) - if sender = User.find(sender_id) - address = default_sender_address - address.display_name = sender.name - address.format + def sender(sender_id, send_from_user_email = false) + return unless sender = User.find(sender_id) + + address = default_sender_address + address.display_name = sender.name + + if send_from_user_email && can_send_from_user_email?(sender) + address.address = sender.email end + + address.format end # Look up a User by their ID and return their email address @@ -51,9 +81,8 @@ class Notify < ActionMailer::Base # # Returns a String containing the User's email address. def recipient(recipient_id) - if recipient = User.find(recipient_id) - recipient.email - end + @current_user = User.find(recipient_id) + @current_user.notification_email end # Set the References header field @@ -103,6 +132,7 @@ class Notify < ActionMailer::Base # See: mail_answer_thread def mail_new_thread(model, headers = {}, &block) headers['Message-ID'] = message_id(model) + headers['X-GitLab-Project'] = "#{@project.name} | " if @project mail(headers, &block) end @@ -117,11 +147,16 @@ class Notify < ActionMailer::Base def mail_answer_thread(model, headers = {}, &block) headers['In-Reply-To'] = message_id(model) headers['References'] = message_id(model) + headers['X-GitLab-Project'] = "#{@project.name} | " if @project - if (headers[:subject]) + if headers[:subject] headers[:subject].prepend('Re: ') end mail(headers, &block) end + + def can? + Ability.abilities.allowed?(user, action, subject) + end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 97a72bf3635..a5db22040e0 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -14,7 +14,7 @@ class Ability when "MergeRequest" then merge_request_abilities(user, subject) when "Group" then group_abilities(user, subject) when "Namespace" then namespace_abilities(user, subject) - when "GroupMember" then users_group_abilities(user, subject) + when "GroupMember" then group_member_abilities(user, subject) else [] end.concat(global_abilities(user)) end @@ -37,7 +37,7 @@ class Ability :read_issue, :read_milestone, :read_project_snippet, - :read_team_member, + :read_project_member, :read_merge_request, :read_note, :download_code @@ -73,34 +73,55 @@ class Ability # Rules based on role in project if team.master?(user) - rules += project_master_rules + rules.push(*project_master_rules) elsif team.developer?(user) - rules += project_dev_rules + rules.push(*project_dev_rules) elsif team.reporter?(user) - rules += project_report_rules + rules.push(*project_report_rules) elsif team.guest?(user) - rules += project_guest_rules + rules.push(*project_guest_rules) end if project.public? || project.internal? - rules += public_project_rules + rules.push(*public_project_rules) end if project.owner == user || user.admin? - rules += project_admin_rules + rules.push(*project_admin_rules) end if project.group && project.group.has_owner?(user) - rules += project_admin_rules + rules.push(*project_admin_rules) end if project.archived? rules -= project_archived_rules end + unless project.issues_enabled + rules -= named_abilities('issue') + end + + unless project.merge_requests_enabled + rules -= named_abilities('merge_request') + end + + unless project.issues_enabled or project.merge_requests_enabled + rules -= named_abilities('label') + rules -= named_abilities('milestone') + end + + unless project.snippets_enabled + rules -= named_abilities('project_snippet') + end + + unless project.wiki_enabled + rules -= named_abilities('wiki') + end + rules end end @@ -117,9 +138,10 @@ class Ability :read_project, :read_wiki, :read_issue, + :read_label, :read_milestone, :read_project_snippet, - :read_team_member, + :read_project_member, :read_merge_request, :read_note, :write_project, @@ -166,7 +188,7 @@ class Ability :admin_issue, :admin_milestone, :admin_project_snippet, - :admin_team_member, + :admin_project_member, :admin_merge_request, :admin_note, :admin_wiki, @@ -193,17 +215,17 @@ class Ability # Only group masters and group owners can create new projects in group if group.has_master?(user) || group.has_owner?(user) || user.admin? - rules += [ + rules.push(*[ :create_projects, - ] + ]) end - # Only group owner and administrators can manage group + # Only group owner and administrators can admin group if group.has_owner?(user) || user.admin? - rules += [ - :manage_group, - :manage_namespace - ] + rules.push(*[ + :admin_group, + :admin_namespace + ]) end rules.flatten @@ -212,12 +234,12 @@ class Ability def namespace_abilities(user, namespace) rules = [] - # Only namespace owner and administrators can manage it + # Only namespace owner and administrators can admin it if namespace.owner == user || user.admin? - rules += [ + rules.push(*[ :create_projects, - :manage_namespace - ] + :admin_namespace + ]) end rules.flatten @@ -225,13 +247,15 @@ class Ability [:issue, :note, :project_snippet, :personal_snippet, :merge_request].each do |name| define_method "#{name}_abilities" do |user, subject| - if subject.author == user - [ + if subject.author == user || user.is_admin? + rules = [ :"read_#{name}", :"write_#{name}", :"modify_#{name}", :"admin_#{name}" ] + rules.push(:change_visibility_level) if subject.is_a?(Snippet) + rules elsif subject.respond_to?(:assignee) && subject.assignee == user [ :"read_#{name}", @@ -239,7 +263,7 @@ class Ability :"modify_#{name}", ] else - if subject.respond_to?(:project) + if subject.respond_to?(:project) && subject.project project_abilities(user, subject.project) else [] @@ -248,17 +272,17 @@ class Ability end end - def users_group_abilities(user, subject) + def group_member_abilities(user, subject) rules = [] target_user = subject.user group = subject.group - can_manage = group_abilities(user, group).include?(:manage_group) + can_manage = group_abilities(user, group).include?(:admin_group) if can_manage && (user != target_user) - rules << :modify - rules << :destroy + rules << :modify_group_member + rules << :destroy_group_member end if !group.last_owner?(user) && (can_manage || (user == target_user)) - rules << :destroy + rules << :destroy_group_member end rules end @@ -270,5 +294,16 @@ class Ability abilities end end + + private + + def named_abilities(name) + [ + :"read_#{name}", + :"write_#{name}", + :"modify_#{name}", + :"admin_#{name}" + ] + end end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb new file mode 100644 index 00000000000..fee52694099 --- /dev/null +++ b/app/models/application_setting.rb @@ -0,0 +1,96 @@ +# == 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) +# twitter_sharing_enabled :boolean default(TRUE) +# restricted_visibility_levels :text +# max_attachment_size :integer default(10), not null +# session_expire_delay :integer default(10080), not null +# default_project_visibility :integer +# default_snippet_visibility :integer +# restricted_signup_domains :text +# user_oauth_applications :bool default(TRUE) +# after_sign_out_path :string(255) +# + +class ApplicationSetting < ActiveRecord::Base + serialize :restricted_visibility_levels + serialize :restricted_signup_domains, Array + attr_accessor :restricted_signup_domains_raw + + validates :session_expire_delay, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + + validates :home_page_url, + allow_blank: true, + format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, + if: :home_page_url_column_exist + + validates :after_sign_out_path, + allow_blank: true, + format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } + + validates_each :restricted_visibility_levels do |record, attr, value| + unless value.nil? + value.each do |level| + unless Gitlab::VisibilityLevel.options.has_value?(level) + record.errors.add(attr, "'#{level}' is not a valid visibility level") + end + end + end + end + + def self.current + ApplicationSetting.last + end + + def self.create_from_defaults + create( + default_projects_limit: Settings.gitlab['default_projects_limit'], + default_branch_protection: Settings.gitlab['default_branch_protection'], + signup_enabled: Settings.gitlab['signup_enabled'], + signin_enabled: Settings.gitlab['signin_enabled'], + twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], + gravatar_enabled: Settings.gravatar['enabled'], + sign_in_text: Settings.extra['sign_in_text'], + restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], + max_attachment_size: Settings.gitlab['max_attachment_size'], + session_expire_delay: Settings.gitlab['session_expire_delay'], + default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], + default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], + restricted_signup_domains: Settings.gitlab['restricted_signup_domains'] + ) + end + + def home_page_url_column_exist + ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) + end + + def restricted_signup_domains_raw + self.restricted_signup_domains.join("\n") unless self.restricted_signup_domains.nil? + end + + def restricted_signup_domains_raw=(values) + self.restricted_signup_domains = [] + self.restricted_signup_domains = values.split( + /\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace + | # or + \s # any whitespace character + | # or + [\r\n] # any number of newline characters + /x) + self.restricted_signup_domains.reject! { |d| d.empty? } + end + +end diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 4d0c04bcc3d..05f5e979695 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -14,6 +14,8 @@ # class BroadcastMessage < ActiveRecord::Base + include Sortable + validates :message, presence: true validates :starts_at, presence: true validates :ends_at, presence: true diff --git a/app/models/commit.rb b/app/models/commit.rb index 212229649fc..9d721661629 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -1,29 +1,35 @@ class Commit - include ActiveModel::Conversion - include StaticModel extend ActiveModel::Naming + + include ActiveModel::Conversion include Mentionable + include Participable + include Referable + include StaticModel attr_mentionable :safe_message + participant :author, :committer, :notes, :mentioned_users + + attr_accessor :project # Safe amount of changes (files and lines) in one commit to render # Used to prevent 500 error on huge commits by suppressing diff # # User can force display of diff above this size - DIFF_SAFE_FILES = 100 - DIFF_SAFE_LINES = 5000 + DIFF_SAFE_FILES = 100 unless defined?(DIFF_SAFE_FILES) + DIFF_SAFE_LINES = 5000 unless defined?(DIFF_SAFE_LINES) # Commits above this size will not be rendered in HTML - DIFF_HARD_LIMIT_FILES = 1000 - DIFF_HARD_LIMIT_LINES = 50000 + DIFF_HARD_LIMIT_FILES = 1000 unless defined?(DIFF_HARD_LIMIT_FILES) + DIFF_HARD_LIMIT_LINES = 50000 unless defined?(DIFF_HARD_LIMIT_LINES) class << self - def decorate(commits) + def decorate(commits, project) commits.map do |commit| if commit.kind_of?(Commit) commit else - self.new(commit) + self.new(commit, project) end end end @@ -41,16 +47,45 @@ class Commit attr_accessor :raw - def initialize(raw_commit) + def initialize(raw_commit, project) raise "Nil as raw commit passed" unless raw_commit @raw = raw_commit + @project = project end def id @raw.id end + def ==(other) + (self.class === other) && (raw == other.raw) + end + + def self.reference_prefix + '@' + end + + # Pattern used to extract commit references from text + # + # The SHA can be between 6 and 40 hex characters. + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (?:#{Project.reference_pattern}#{reference_prefix})? + (?<commit>\h{6,40}) + }x + end + + def to_reference(from_project = nil) + if cross_project_reference?(from_project) + "#{project.to_reference}@#{id}" + else + id + end + end + def diff_line_count @diff_line_count ||= Commit::diff_line_count(self.diffs) @diff_line_count @@ -75,11 +110,11 @@ class Commit return no_commit_message if title.blank? - title_end = title.index(/\n/) + title_end = title.index("\n") if (!title_end && title.length > 100) || (title_end && title_end > 100) - title[0..79] << "…".html_safe + title[0..79] << "…" else - title.split(/\n/, 2).first + title.split("\n", 2).first end end @@ -87,19 +122,20 @@ class Commit # # cut off, ellipses (`&hellp;`) are prepended to the commit message. def description - title_end = safe_message.index(/\n/) - @description ||= if (!title_end && safe_message.length > 100) || (title_end && title_end > 100) - "…".html_safe << safe_message[80..-1] - else - safe_message.split(/\n/, 2)[1].try(:chomp) - end + title_end = safe_message.index("\n") + @description ||= + if (!title_end && safe_message.length > 100) || (title_end && title_end > 100) + "…" << safe_message[80..-1] + else + safe_message.split("\n", 2)[1].try(:chomp) + end end def description? description.present? end - def hook_attrs(project) + def hook_attrs path_with_namespace = project.path_with_namespace { @@ -116,23 +152,28 @@ class Commit # Discover issues should be closed when this commit is pushed to a project's # default branch. - def closes_issues(project) - Gitlab::ClosingIssueExtractor.closed_by_message_in_project(safe_message, project) + def closes_issues(current_user = self.committer) + Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message) + end + + def author + User.find_for_commit(author_email, author_name) end - # Mentionable override. - def gfm_reference - "commit #{id}" + def committer + User.find_for_commit(committer_email, committer_name) + end + + def notes + project.notes.for_commit_id(self.id) end def method_missing(m, *args, &block) @raw.send(m, *args, &block) end - def respond_to?(method) - return true if @raw.respond_to?(method) - - super + def respond_to_missing?(method, include_private = false) + @raw.respond_to?(method, include_private) || super end # Truncate sha to 8 characters @@ -141,6 +182,6 @@ class Commit end def parents - @parents ||= Commit.decorate(super) + @parents ||= Commit.decorate(super, project) end end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb new file mode 100644 index 00000000000..86fc9eb01a3 --- /dev/null +++ b/app/models/commit_range.rb @@ -0,0 +1,132 @@ +# CommitRange makes it easier to work with commit ranges +# +# Examples: +# +# range = CommitRange.new('f3f85602...e86e1013') +# range.exclude_start? # => false +# range.reference_title # => "Commits f3f85602 through e86e1013" +# range.to_s # => "f3f85602...e86e1013" +# +# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae') +# range.exclude_start? # => true +# range.reference_title # => "Commits f3f85602^ through e86e1013" +# range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"} +# range.to_s # => "f3f85602..e86e1013" +# +# # Assuming `project` is a Project with a repository containing both commits: +# range.project = project +# range.valid_commits? # => true +# +class CommitRange + include ActiveModel::Conversion + include Referable + + attr_reader :sha_from, :notation, :sha_to + + # Optional Project model + attr_accessor :project + + # See `exclude_start?` + attr_reader :exclude_start + + # The beginning and ending SHAs can be between 6 and 40 hex characters, and + # the range notation can be double- or triple-dot. + PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ + + def self.reference_prefix + '@' + end + + # Pattern used to extract commit range references from text + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (?:#{Project.reference_pattern}#{reference_prefix})? + (?<commit_range>#{PATTERN}) + }x + end + + # Initialize a CommitRange + # + # range_string - The String commit range. + # project - An optional Project model. + # + # Raises ArgumentError if `range_string` does not match `PATTERN`. + def initialize(range_string, project = nil) + range_string.strip! + + unless range_string.match(/\A#{PATTERN}\z/) + raise ArgumentError, "invalid CommitRange string format: #{range_string}" + end + + @exclude_start = !range_string.include?('...') + @sha_from, @notation, @sha_to = range_string.split(/(\.{2,3})/, 2) + + @project = project + end + + def inspect + %(#<#{self.class}:#{object_id} #{to_s}>) + end + + def to_s + "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}" + end + + def to_reference(from_project = nil) + # Not using to_s because we want the full SHAs + reference = sha_from + notation + sha_to + + if cross_project_reference?(from_project) + reference = project.to_reference + '@' + reference + end + + reference + end + + # Returns a String for use in a link's title attribute + def reference_title + "Commits #{suffixed_sha_from} through #{sha_to}" + end + + # Return a Hash of parameters for passing to a URL helper + # + # See `namespace_project_compare_url` + def to_param + { from: suffixed_sha_from, to: sha_to } + end + + def exclude_start? + exclude_start + end + + # Check if both the starting and ending commit IDs exist in a project's + # repository + # + # project - An optional Project to check (default: `project`) + def valid_commits?(project = project) + return nil unless project.present? + return false unless project.valid_repo? + + commit_from.present? && commit_to.present? + end + + def persisted? + true + end + + def commit_from + @commit_from ||= project.repository.commit(suffixed_sha_from) + end + + def commit_to + @commit_to ||= project.repository.commit(sha_to) + end + + private + + def suffixed_sha_from + sha_from + (exclude_start? ? '^' : '') + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index f49708fd6eb..97846b06d72 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -7,6 +7,7 @@ module Issuable extend ActiveSupport::Concern include Mentionable + include Participable included do belongs_to :author, class_name: "User" @@ -15,6 +16,7 @@ module Issuable has_many :notes, as: :noteable, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links + has_many :subscriptions, dependent: :destroy, as: :subscribable validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } @@ -29,6 +31,8 @@ module Issuable scope :only_opened, -> { with_state(:opened) } scope :only_reopened, -> { with_state(:reopened) } scope :closed, -> { with_state(:closed) } + scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') } + scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') } delegate :name, :email, @@ -42,6 +46,7 @@ module Issuable prefix: true attr_mentionable :title, :description + participant :author, :assignee, :notes, :mentioned_users end module ClassMethods @@ -55,13 +60,10 @@ module Issuable def sort(method) case method.to_s - when 'newest' then reorder("#{table_name}.created_at DESC") - when 'oldest' then reorder("#{table_name}.created_at ASC") - when 'recently_updated' then reorder("#{table_name}.updated_at DESC") - when 'last_updated' then reorder("#{table_name}.updated_at ASC") - when 'milestone_due_soon' then joins(:milestone).reorder("milestones.due_date ASC") - when 'milestone_due_later' then joins(:milestone).reorder("milestones.due_date DESC") - else reorder("#{table_name}.created_at DESC") + when 'milestone_due_asc' then order_milestone_due_asc + when 'milestone_due_desc' then order_milestone_due_desc + else + order_by(method) end end end @@ -88,7 +90,7 @@ module Issuable # Return the number of -1 comments (downvotes) def downvotes - notes.select(&:downvote?).size + filter_superceded_votes(notes.select(&:downvote?), notes).size end def downvotes_in_percent @@ -101,7 +103,7 @@ module Issuable # Return the number of +1 comments (upvotes) def upvotes - notes.select(&:upvote?).size + filter_superceded_votes(notes.select(&:upvote?), notes).size end def upvotes_in_percent @@ -117,18 +119,20 @@ module Issuable upvotes + downvotes end - # Return all users participating on the discussion - def participants - users = [] - users << author - users << assignee if is_assigned? - mentions = [] - mentions << self.mentioned_users - notes.each do |note| - users << note.author - mentions << note.mentioned_users + def subscribed?(user) + subscription = subscriptions.find_by_user_id(user.id) + + if subscription + return subscription.subscribed end - users.concat(mentions.reduce([], :|)).uniq + + participants(user).include?(user) + end + + def toggle_subscription(user) + subscriptions. + find_or_initialize_by(user_id: user.id). + update(subscribed: !subscribed?(user)) end def to_hook_data(user) @@ -149,9 +153,23 @@ module Issuable def add_labels_by_names(label_names) label_names.each do |label_name| - label = project.labels.create_with( - color: Label::DEFAULT_COLOR).find_or_create_by(title: label_name.strip) + label = project.labels.create_with(color: Label::DEFAULT_COLOR). + find_or_create_by(title: label_name.strip) self.labels << label end end + + private + + def filter_superceded_votes(votes, notes) + filteredvotes = [] + votes + + votes.each do |vote| + if vote.superceded?(notes) + filteredvotes.delete(vote) + end + end + + filteredvotes + end end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 6c1aa99668a..56849f28ff0 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -20,15 +20,20 @@ module Mentionable end end - # Generate a GFM back-reference that will construct a link back to this Mentionable when rendered. Must - # be overridden if this model object can be referenced directly by GFM notation. - def gfm_reference - raise NotImplementedError.new("#{self.class} does not implement #gfm_reference") + # Returns the text used as the body of a Note when this object is referenced + # + # By default this will be the class name and the result of calling + # `to_reference` on the object. + def gfm_reference(from_project = nil) + # "MergeRequest" > "merge_request" > "Merge request" > "merge request" + friendly_name = self.class.to_s.underscore.humanize.downcase + + "#{friendly_name} #{to_reference(from_project)}" end # Construct a String that contains possible GFM references. def mentionable_text - self.class.mentionable_attrs.map { |attr| send(attr) || '' }.join + self.class.mentionable_attrs.map { |attr| send(attr) }.compact.join("\n\n") end # The GFM reference to this Mentionable, which shouldn't be included in its #references. @@ -39,41 +44,38 @@ module Mentionable # Determine whether or not a cross-reference Note has already been created between this Mentionable and # the specified target. def has_mentioned?(target) - Note.cross_reference_exists?(target, local_reference) + SystemNoteService.cross_reference_exists?(target, local_reference) end - def mentioned_users - users = [] - return users if mentionable_text.blank? - has_project = self.respond_to? :project - matches = mentionable_text.scan(/@[a-zA-Z][a-zA-Z0-9_\-\.]*/) - matches.each do |match| - identifier = match.delete "@" - if identifier == "all" - users += project.team.members.flatten - else - id = User.find_by(username: identifier).try(:id) - users << User.find(id) unless id.blank? - end - end - users.uniq + def mentioned_users(current_user = nil) + return [] if mentionable_text.blank? + + ext = Gitlab::ReferenceExtractor.new(self.project, current_user) + ext.analyze(mentionable_text) + ext.users.uniq end # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. - def references(p = project, text = mentionable_text) + def references(p = project, current_user = self.author, text = mentionable_text) return [] if text.blank? - ext = Gitlab::ReferenceExtractor.new - ext.analyze(text, p) - (ext.issues_for + - ext.merge_requests_for + - ext.commits_for).uniq - [local_reference] + + ext = Gitlab::ReferenceExtractor.new(p, current_user) + ext.analyze(text) + + (ext.issues + ext.merge_requests + ext.commits).uniq - [local_reference] end # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. def create_cross_references!(p = project, a = author, without = []) - refs = references(p) - without + refs = references(p) + + # We're using this method instead of Array diffing because that requires + # both of the object's `hash` values to be the same, which may not be the + # case for otherwise identical Commit objects. + refs.reject! { |ref| without.include?(ref) } + refs.each do |ref| - Note.create_cross_reference_note(ref, local_reference, a, p) + SystemNoteService.cross_reference(ref, local_reference, a) end end @@ -92,8 +94,7 @@ module Mentionable # Only proceed if the saved changes actually include a chance to an attr_mentionable field. return unless mentionable_changed - preexisting = references(p, original) + preexisting = references(p, self.author, original) create_cross_references!(p, a, preexisting) end - end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb new file mode 100644 index 00000000000..9f667f47e0d --- /dev/null +++ b/app/models/concerns/participable.rb @@ -0,0 +1,73 @@ +# == Participable concern +# +# 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 +# include Participable +# +# # ... +# +# participant :author, :assignee, :mentioned_users, :notes +# 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.map(&:to_s)) + end + + def participant_attrs + @participant_attrs ||= [] + end + end + + def participants(current_user = self.author, project = self.project) + participants = self.class.participant_attrs.flat_map do |attr| + meth = method(attr) + + value = + if meth.arity == 1 || meth.arity == -1 + meth.call(current_user) + else + meth.call + end + + participants_for(value, current_user, project) + end.compact.uniq + + if project + participants.select! do |user| + user.can?(:read_project, project) + end + end + + participants + end + + private + + def participants_for(value, current_user = nil, project = nil) + case value + when User + [value] + when Enumerable, ActiveRecord::Relation + value.flat_map { |v| participants_for(v, current_user, project) } + when Participable + value.participants(current_user, project) + end + end +end diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb new file mode 100644 index 00000000000..cced66cc1e4 --- /dev/null +++ b/app/models/concerns/referable.rb @@ -0,0 +1,61 @@ +# == Referable concern +# +# Contains functionality related to making a model referable in Markdown, such +# as "#1", "!2", "~3", etc. +module Referable + extend ActiveSupport::Concern + + # Returns the String necessary to reference this object in Markdown + # + # from_project - Refering Project object + # + # This should be overridden by the including class. + # + # Examples: + # + # Issue.first.to_reference # => "#1" + # Issue.last.to_reference(other_project) # => "cross-project#1" + # + # Returns a String + def to_reference(_from_project = nil) + '' + end + + module ClassMethods + # The character that prefixes the actual reference identifier + # + # This should be overridden by the including class. + # + # Examples: + # + # Issue.reference_prefix # => '#' + # MergeRequest.reference_prefix # => '!' + # + # Returns a String + def reference_prefix + '' + end + + # Regexp pattern used to match references to this object + # + # This must be overridden by the including class. + # + # Returns a Regexp + def reference_pattern + raise NotImplementedError, "#{self} does not implement #{__method__}" + end + end + + private + + # Check if a reference is being done cross-project + # + # from_project - Refering Project object + def cross_project_reference?(from_project) + if self.is_a?(Project) + self != from_project + else + from_project && self.project && self.project != from_project + end + end +end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb new file mode 100644 index 00000000000..0ad2654867d --- /dev/null +++ b/app/models/concerns/sortable.rb @@ -0,0 +1,35 @@ +# == Sortable concern +# +# Set default scope for ordering objects +# +module Sortable + extend ActiveSupport::Concern + + included do + # By default all models should be ordered + # by created_at field starting from newest + default_scope { order(created_at: :desc, id: :desc) } + + scope :order_created_desc, -> { reorder(created_at: :desc, id: :desc) } + scope :order_created_asc, -> { reorder(created_at: :asc, id: :asc) } + scope :order_updated_desc, -> { reorder(updated_at: :desc, id: :desc) } + scope :order_updated_asc, -> { reorder(updated_at: :asc, id: :asc) } + scope :order_name_asc, -> { reorder(name: :asc) } + scope :order_name_desc, -> { reorder(name: :desc) } + end + + module ClassMethods + def order_by(method) + case method.to_s + when 'name_asc' then order_name_asc + when 'name_desc' then order_name_desc + when 'updated_asc' then order_updated_asc + when 'updated_desc' then order_updated_desc + when 'created_asc' then order_created_asc + when 'created_desc' then order_created_desc + else + all + end + end + end +end diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index 410e8dc820b..660e58b876d 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -1,51 +1,37 @@ +require 'task_list' +require 'task_list/filter' + # Contains functionality for objects that can have task lists in their # descriptions. Task list items can be added with Markdown like "* [x] Fix # bugs". # # Used by MergeRequest and Issue module Taskable - TASK_PATTERN_MD = /^(?<bullet> *[*-] *)\[(?<checked>[ xX])\]/.freeze - TASK_PATTERN_HTML = /^<li>\[(?<checked>[ xX])\]/.freeze - - # Change the state of a task list item for this Taskable. Edit the object's - # description by finding the nth task item and changing its checkbox - # placeholder to "[x]" if +checked+ is true, or "[ ]" if it's false. - # Note: task numbering starts with 1 - def update_nth_task(n, checked) - index = 0 - check_char = checked ? 'x' : ' ' + # Called by `TaskList::Summary` + def task_list_items + return [] if description.blank? - # Do this instead of using #gsub! so that ActiveRecord detects that a field - # has changed. - self.description = self.description.gsub(TASK_PATTERN_MD) do |match| - index += 1 - case index - when n then "#{$LAST_MATCH_INFO[:bullet]}[#{check_char}]" - else match - end + @task_list_items ||= description.scan(TaskList::Filter::ItemPattern).collect do |item| + # ItemPattern strips out the hyphen, but Item requires it. Rabble rabble. + TaskList::Item.new("- #{item}") end + end - save + def tasks + @tasks ||= TaskList.new(self) end # Return true if this object's description has any task list items. def tasks? - description && description.match(TASK_PATTERN_MD) + tasks.summary.items? end # Return a string that describes the current state of this Taskable's task - # list items, e.g. "20 tasks (12 done, 8 unfinished)" + # list items, e.g. "20 tasks (12 completed, 8 remaining)" def task_status - return nil unless description - - num_tasks = 0 - num_done = 0 - - description.scan(TASK_PATTERN_MD) do - num_tasks += 1 - num_done += 1 unless $LAST_MATCH_INFO[:checked] == ' ' - end + return '' if description.blank? - "#{num_tasks} tasks (#{num_done} done, #{num_tasks - num_done} unfinished)" + sum = tasks.summary + "#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)" end end diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 570f5e91c13..9ab663c04ad 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -10,6 +10,7 @@ # title :string(255) # type :string(255) # fingerprint :string(255) +# public :boolean default(FALSE), not null # class DeployKey < Key @@ -17,4 +18,21 @@ class DeployKey < Key has_many :projects, through: :deploy_keys_projects scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) } + scope :are_public, -> { where(public: true) } + + def private? + !public? + end + + def orphaned? + self.deploy_keys_projects.length == 0 + end + + def almost_orphaned? + self.deploy_keys_projects.length == 1 + end + + def destroyed_when_orphaned? + self.private? + end end diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index f23d8205ddc..18db521741f 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -16,4 +16,14 @@ class DeployKeysProject < ActiveRecord::Base validates :deploy_key_id, presence: true validates :deploy_key_id, uniqueness: { scope: [:project_id], message: "already exists in project" } validates :project_id, presence: true + + after_destroy :destroy_orphaned_deploy_key + + private + + def destroy_orphaned_deploy_key + return unless self.deploy_key.destroyed_when_orphaned? && self.deploy_key.orphaned? + + self.deploy_key.destroy + end end diff --git a/app/models/email.rb b/app/models/email.rb index 57f476bd519..935705e2ed4 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -10,13 +10,14 @@ # class Email < ActiveRecord::Base + include Sortable + belongs_to :user validates :user_id, presence: true validates :email, presence: true, email: { strict_mode: true }, uniqueness: true validate :unique_email, if: ->(email) { email.email_changed? } - after_create :notify before_validation :cleanup_email def cleanup_email @@ -26,8 +27,4 @@ class Email < ActiveRecord::Base def unique_email self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email) end - - def notify - NotificationService.new.new_email(self) - end end diff --git a/app/models/event.rb b/app/models/event.rb index 65b4c2edfee..c9a88ffa8e0 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -15,6 +15,7 @@ # class Event < ActiveRecord::Base + include Sortable default_scope { where.not(author_id: nil) } CREATED = 1 @@ -46,36 +47,20 @@ class Event < ActiveRecord::Base scope :recent, -> { order("created_at DESC") } scope :code_push, -> { where(action: PUSHED) } scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent } + scope :with_associations, -> { includes(project: :namespace) } class << self - def create_ref_event(project, user, ref, action = 'add', prefix = 'refs/heads') - commit = project.repository.commit(ref.target) - - if action.to_s == 'add' - before = '00000000' - after = commit.id - else - before = commit.id - after = '00000000' - end - - Event.create( - project: project, - action: Event::PUSHED, - data: { - ref: "#{prefix}/#{ref.name}", - before: before, - after: after - }, - author_id: user.id - ) - end - def reset_event_cache_for(target) Event.where(target_id: target.id, target_type: target.class.to_s). order('id DESC').limit(100). update_all(updated_at: Time.now) end + + def contributions + where("action = ? OR (target_type in (?) AND action in (?))", + Event::PUSHED, ["MergeRequest", "Issue"], + [Event::CREATED, Event::CLOSED, Event::MERGED]) + end end def proper? @@ -83,6 +68,8 @@ class Event < ActiveRecord::Base true elsif membership_changed? true + elsif created_project? + true else (issue? || merge_request? || note? || milestone?) && target end @@ -97,25 +84,51 @@ class Event < ActiveRecord::Base end def target_title - if target && target.respond_to?(:title) - target.title - end + target.title if target && target.respond_to?(:title) + end + + def created? + action == CREATED end def push? - action == self.class::PUSHED && valid_push? + action == PUSHED && valid_push? end def merged? - action == self.class::MERGED + action == MERGED end def closed? - action == self.class::CLOSED + action == CLOSED end def reopened? - action == self.class::REOPENED + action == REOPENED + end + + def joined? + action == JOINED + end + + def left? + action == LEFT + end + + def commented? + action == COMMENTED + end + + def membership_changed? + joined? || left? + end + + def created_project? + created? && !target + end + + def created_target? + created? && target end def milestone? @@ -134,32 +147,32 @@ class Event < ActiveRecord::Base target_type == "MergeRequest" end - def joined? - action == JOINED - end - - def left? - action == LEFT - end - - def membership_changed? - joined? || left? + def milestone + target if milestone? end def issue - target if target_type == "Issue" + target if issue? end def merge_request - target if target_type == "MergeRequest" + target if merge_request? end def note - target if target_type == "Note" + target if note? end def action_name - if closed? + if push? + if new_ref? + "pushed new" + elsif rm_ref? + "deleted" + else + "pushed to" + end + elsif closed? "closed" elsif merged? "accepted" @@ -167,6 +180,14 @@ class Event < ActiveRecord::Base 'joined' elsif left? 'left' + elsif commented? + "commented on" + elsif created_project? + if project.import? + "imported" + else + "created" + end else "opened" end @@ -174,24 +195,24 @@ class Event < ActiveRecord::Base def valid_push? data[:ref] && ref_name.present? - rescue => ex + rescue false end def tag? - data[:ref]["refs/tags"] + Gitlab::Git.tag_ref?(data[:ref]) end def branch? - data[:ref]["refs/heads"] + Gitlab::Git.branch_ref?(data[:ref]) end def new_ref? - commit_from =~ /^00000/ + Gitlab::Git.blank_ref?(commit_from) end def rm_ref? - commit_to =~ /^00000/ + Gitlab::Git.blank_ref?(commit_to) end def md_ref? @@ -215,11 +236,11 @@ class Event < ActiveRecord::Base end def branch_name - @branch_name ||= data[:ref].gsub("refs/heads/", "") + @branch_name ||= Gitlab::Git.ref_name(data[:ref]) end def tag_name - @tag_name ||= data[:ref].gsub("refs/tags/", "") + @tag_name ||= Gitlab::Git.ref_name(data[:ref]) end # Max 20 commits from push DESC @@ -235,18 +256,8 @@ class Event < ActiveRecord::Base tag? ? "tag" : "branch" end - def push_action_name - if new_ref? - "pushed new" - elsif rm_ref? - "deleted" - else - "pushed to" - end - end - def push_with_commits? - md_ref? && commits.any? && commit_from && commit_to + !commits.empty? && commit_from && commit_to end def last_push_to_non_root? diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb new file mode 100644 index 00000000000..49f6c95e045 --- /dev/null +++ b/app/models/external_issue.rb @@ -0,0 +1,40 @@ +class ExternalIssue + include Referable + + def initialize(issue_identifier, project) + @issue_identifier, @project = issue_identifier, project + end + + def to_s + @issue_identifier.to_s + end + + def id + @issue_identifier.to_s + end + + def iid + @issue_identifier.to_s + end + + def title + "External Issue #{self}" + end + + def ==(other) + other.is_a?(self.class) && (to_s == other.to_s) + end + + def project + @project + end + + # Pattern used to extract `JIRA-123` issue references from text + def self.reference_pattern + %r{(?<issue>([A-Z\-]+-)\d+)} + end + + def to_reference(_from_project = nil) + id + end +end diff --git a/app/models/group.rb b/app/models/group.rb index b8ed3b8ac73..051c672cb33 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -17,13 +17,40 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Group < Namespace + include Referable + has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' has_many :users, through: :group_members - validate :avatar_type, if: ->(user) { user.avatar_changed? } - validates :avatar, file_size: { maximum: 100.kilobytes.to_i } + validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } + validates :avatar, file_size: { maximum: 200.kilobytes.to_i } + + mount_uploader :avatar, AvatarUploader + + after_create :post_create_hook + after_destroy :post_destroy_hook + + class << self + def search(query) + where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%") + end + + def sort(method) + order_by(method) + end + + def reference_prefix + User.reference_prefix + end + + def reference_pattern + User.reference_pattern + end + end - mount_uploader :avatar, AttachmentUploader + def to_reference(_from_project = nil) + "#{self.class.reference_prefix}#{name}" + end def human_name name @@ -33,19 +60,18 @@ class Group < Namespace @owners ||= group_members.owners.map(&:user) end - def add_users(user_ids, access_level) - user_ids.compact.each do |user_id| - user = self.group_members.find_or_initialize_by(user_id: user_id) - user.update_attributes(access_level: access_level) + def add_users(user_ids, access_level, current_user = nil) + user_ids.each do |user_id| + Member.add_user(self.group_members, user_id, access_level, current_user) end end - def add_user(user, access_level) - self.group_members.create(user_id: user.id, access_level: access_level) + def add_user(user, access_level, current_user = nil) + add_users([user], access_level, current_user) end - def add_owner(user) - self.add_user(user, Gitlab::Access::OWNER) + def add_owner(user, current_user = nil) + self.add_user(user, Gitlab::Access::OWNER, current_user) end def has_owner?(user) @@ -74,19 +100,19 @@ class Group < Namespace projects.public_only.any? end - class << self - def search(query) - where("LOWER(namespaces.name) LIKE :query", query: "%#{query.downcase}%") - end + def post_create_hook + Gitlab::AppLogger.info("Group \"#{name}\" was created") - def sort(method) - case method.to_s - when "newest" then reorder("namespaces.created_at DESC") - when "oldest" then reorder("namespaces.created_at ASC") - when "recently_updated" then reorder("namespaces.updated_at DESC") - when "last_updated" then reorder("namespaces.updated_at ASC") - else reorder("namespaces.path, namespaces.name ASC") - end - end + system_hook_service.execute_hooks_for(self, :create) + end + + def post_destroy_hook + Gitlab::AppLogger.info("Group \"#{name}\" was removed") + + system_hook_service.execute_hooks_for(self, :destroy) + end + + def system_hook_service + SystemHooksService.new end end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb index 33915313789..ab055f6b80b 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -44,7 +44,7 @@ class GroupMilestone def percent_complete ((closed_items_count * 100) / total_items_count).abs rescue ZeroDivisionError - 100 + 0 end def state @@ -66,15 +66,15 @@ class GroupMilestone end def issues - @group_issues ||= milestones.map { |milestone| milestone.issues }.flatten.group_by(&:state) + @group_issues ||= milestones.map(&:issues).flatten.group_by(&:state) end def merge_requests - @group_merge_requests ||= milestones.map { |milestone| milestone.merge_requests }.flatten.group_by(&:state) + @group_merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state) end def participants - milestones.map { |milestone| milestone.participants.uniq }.reject(&:empty?).flatten + @group_participants ||= milestones.map(&:participants).flatten.compact.uniq end def opened_issues diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 21867a9316c..ca7066b959a 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -13,6 +13,7 @@ # 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 # class ProjectHook < WebHook @@ -21,5 +22,6 @@ class ProjectHook < WebHook scope :push_hooks, -> { where(push_events: true) } scope :tag_push_hooks, -> { where(tag_push_events: true) } scope :issue_hooks, -> { where(issues_events: true) } + scope :note_hooks, -> { where(note_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) } end diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index 2e11239c40b..b55e217975f 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -13,8 +13,13 @@ # 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 # class ServiceHook < WebHook belongs_to :service + + def execute(data) + super(data, 'service_hook') + end end diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index ee32b49bc66..6fb2d421026 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -13,6 +13,7 @@ # 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 # class SystemHook < WebHook diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 23fa01e0b70..46fb85336e5 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -13,13 +13,16 @@ # 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 # class WebHook < ActiveRecord::Base + include Sortable include HTTParty default_value_for :push_events, true default_value_for :issues_events, false + default_value_for :note_events, false default_value_for :merge_requests_events, false default_value_for :tag_push_events, false @@ -27,12 +30,18 @@ class WebHook < ActiveRecord::Base default_timeout Gitlab.config.gitlab.webhook_timeout validates :url, presence: true, - format: { with: URI::regexp(%w(http https)), message: "should be a valid url" } + format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } - def execute(data) + def execute(data, hook_name) parsed_url = URI.parse(url) if parsed_url.userinfo.blank? - WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false) + WebHook.post(url, + body: data.to_json, + headers: { + "Content-Type" => "application/json", + "X-Gitlab-Event" => hook_name.singularize.titleize + }, + verify: false) else post_url = url.gsub("#{parsed_url.userinfo}@", "") auth = { @@ -41,13 +50,19 @@ class WebHook < ActiveRecord::Base } WebHook.post(post_url, body: data.to_json, - headers: {"Content-Type" => "application/json"}, + headers: { + "Content-Type" => "application/json", + "X-Gitlab-Event" => hook_name.singularize.titleize + }, verify: false, basic_auth: auth) end + rescue SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e + logger.error("WebHook Error => #{e}") + false end - def async_execute(data) - Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data) + def async_execute(data, hook_name) + Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data, hook_name) end end diff --git a/app/models/identity.rb b/app/models/identity.rb new file mode 100644 index 00000000000..756d19adec7 --- /dev/null +++ b/app/models/identity.rb @@ -0,0 +1,19 @@ +# == 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 +# + +class Identity < ActiveRecord::Base + include Sortable + belongs_to :user + + validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider } + validates :user_id, uniqueness: { scope: :provider } +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 8a9e969248c..2456b7d0dc1 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -21,8 +21,10 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Issue < ActiveRecord::Base - include Issuable include InternalId + include Issuable + include Referable + include Sortable include Taskable ActsAsTaggableOn.strict_case_match = true @@ -31,7 +33,6 @@ class Issue < ActiveRecord::Base validates :project, presence: true scope :of_group, ->(group) { where(project_id: group.project_ids) } - scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) } scope :cared, ->(user) { where(assignee_id: user) } scope :open_for, ->(user) { opened.assigned_to(user) } @@ -53,10 +54,28 @@ class Issue < ActiveRecord::Base attributes end - # Mentionable overrides. + def self.reference_prefix + '#' + end + + # Pattern used to extract `#123` issue references from text + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?<issue>\d+) + }x + end + + def to_reference(from_project = nil) + reference = "#{self.class.reference_prefix}#{iid}" + + if cross_project_reference?(from_project) + reference = project.to_reference + reference + end - def gfm_reference - "issue ##{iid}" + reference end # Reset issue events cache diff --git a/app/models/key.rb b/app/models/key.rb index 095c73d8baf..bbc28678177 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -10,16 +10,17 @@ # title :string(255) # type :string(255) # fingerprint :string(255) +# public :boolean default(FALSE), not null # require 'digest/md5' class Key < ActiveRecord::Base - include Gitlab::Popen + include Sortable belongs_to :user - before_validation :strip_white_space, :generate_fingerpint + before_validation :strip_white_space, :generate_fingerprint validates :title, presence: true, length: { within: 0..255 } validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }, uniqueness: true @@ -76,22 +77,11 @@ class Key < ActiveRecord::Base private - def generate_fingerpint + def generate_fingerprint self.fingerprint = nil - return unless key.present? - - cmd_status = 0 - cmd_output = '' - Tempfile.open('gitlab_key_file') do |file| - file.puts key - file.rewind - cmd_output, cmd_status = popen(%W(ssh-keygen -lf #{file.path}), '/tmp') - end - - if cmd_status.zero? - cmd_output.gsub /([\d\h]{2}:)+[\d\h]{2}/ do |match| - self.fingerprint = match - end - end + + return unless self.key.present? + + self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint end end diff --git a/app/models/label.rb b/app/models/label.rb index 2b2b02e0645..230631b5180 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -11,8 +11,12 @@ # class Label < ActiveRecord::Base + include Referable + DEFAULT_COLOR = '#428BCA' + default_value_for :color, DEFAULT_COLOR + belongs_to :project has_many :label_links, dependent: :destroy has_many :issues, through: :label_links, source: :target, source_type: 'Issue' @@ -25,13 +29,52 @@ class Label < ActiveRecord::Base # Don't allow '?', '&', and ',' for label titles validates :title, presence: true, - format: { with: /\A[^&\?,&]+\z/ }, + format: { with: /\A[^&\?,]+\z/ }, uniqueness: { scope: :project_id } - scope :order_by_name, -> { reorder("labels.title ASC") } + default_scope { order(title: :asc) } alias_attribute :name, :title + def self.reference_prefix + '~' + end + + # Pattern used to extract label references from text + def self.reference_pattern + %r{ + #{reference_prefix} + (?: + (?<label_id>\d+) | # Integer-based label ID, or + (?<label_name> + [A-Za-z0-9_-]+ | # String-based single-word label title, or + "[^&\?,]+" # String-based multi-word label surrounded in quotes + ) + ) + }x + end + + # Returns the String necessary to reference this Label in Markdown + # + # format - Symbol format to use (default: :id, optional: :name) + # + # Note that its argument differs from other objects implementing Referable. If + # a non-Symbol argument is given (such as a Project), it will default to :id. + # + # Examples: + # + # Label.first.to_reference # => "~1" + # Label.first.to_reference(:name) # => "~\"bug\"" + # + # Returns a String + def to_reference(format = :id) + if format == :name && !name.include?('"') + %(#{self.class.reference_prefix}"#{name}") + else + "#{self.class.reference_prefix}#{id}" + end + end + def open_issues_count issues.opened.count end diff --git a/app/models/member.rb b/app/models/member.rb index 671ef466baa..cae8caa23fb 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -6,30 +6,167 @@ # access_level :integer not null # source_id :integer not null # source_type :string(255) not null -# user_id :integer 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 # class Member < ActiveRecord::Base + include Sortable include Notifiable include Gitlab::Access + attr_accessor :raw_invite_token + + belongs_to :created_by, class_name: "User" belongs_to :user belongs_to :source, polymorphic: true - validates :user, presence: true + validates :user, presence: true, unless: :invite? validates :source, presence: true - validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source" } + validates :user_id, uniqueness: { scope: [:source_type, :source_id], + message: "already exists in source", + allow_nil: true } validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true + validates :invite_email, presence: { if: :invite? }, + email: { strict_mode: true, allow_nil: true }, + uniqueness: { scope: [:source_type, :source_id], allow_nil: true } + scope :invite, -> { where(user_id: nil) } + scope :non_invite, -> { where("user_id IS NOT NULL") } scope :guests, -> { where(access_level: GUEST) } scope :reporters, -> { where(access_level: REPORTER) } scope :developers, -> { where(access_level: DEVELOPER) } scope :masters, -> { where(access_level: MASTER) } scope :owners, -> { where(access_level: OWNER) } + before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } + after_create :send_invite, if: :invite? + after_create :post_create_hook, unless: :invite? + after_update :post_update_hook, unless: :invite? + after_destroy :post_destroy_hook, unless: :invite? + delegate :name, :username, :email, to: :user, prefix: true + + class << self + def find_by_invite_token(invite_token) + invite_token = Devise.token_generator.digest(self, :invite_token, invite_token) + find_by(invite_token: invite_token) + end + + # This method is used to find users that have been entered into the "Add members" field. + # These can be the User objects directly, their IDs, their emails, or new emails to be invited. + def user_for_id(user_id) + return user_id if user_id.is_a?(User) + + user = User.find_by(id: user_id) + user ||= User.find_by(email: user_id) + user ||= user_id + user + end + + def add_user(members, user_id, access_level, current_user = nil) + user = user_for_id(user_id) + + # `user` can be either a User object or an email to be invited + if user.is_a?(User) + member = members.find_or_initialize_by(user_id: user.id) + else + member = members.build + member.invite_email = user + end + + member.created_by ||= current_user + member.access_level = access_level + + member.save + end + end + + def invite? + self.invite_token.present? + end + + def accept_invite!(new_user) + return false unless invite? + + self.invite_token = nil + self.invite_accepted_at = Time.now.utc + + self.user = new_user + + saved = self.save + + after_accept_invite if saved + + saved + end + + def decline_invite! + return false unless invite? + + destroyed = self.destroy + + after_decline_invite if destroyed + + destroyed + end + + def generate_invite_token + raw, enc = Devise.token_generator.generate(self.class, :invite_token) + @raw_invite_token = raw + self.invite_token = enc + end + + def generate_invite_token! + generate_invite_token && save(validate: false) + end + + def resend_invite + return unless invite? + + generate_invite_token! unless @raw_invite_token + + send_invite + end + + private + + def send_invite + # override in subclass + end + + def post_create_hook + system_hook_service.execute_hooks_for(self, :create) + end + + def post_update_hook + # override in subclass + end + + def post_destroy_hook + system_hook_service.execute_hooks_for(self, :destroy) + end + + def after_accept_invite + post_create_hook + end + + def after_decline_invite + # override in subclass + end + + def system_hook_service + SystemHooksService.new + end + + def notification_service + NotificationService.new + end end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index b7f296b13fb..65d2ea00570 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -6,11 +6,15 @@ # access_level :integer not null # source_id :integer not null # source_type :string(255) not null -# user_id :integer 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 # class GroupMember < Member @@ -27,9 +31,6 @@ class GroupMember < Member scope :with_group, ->(group) { where(source_id: group.id) } scope :with_user, ->(user) { where(user_id: user.id) } - after_create :notify_create - after_update :notify_update - def self.access_level_roles Gitlab::Access.options_with_owner end @@ -42,17 +43,37 @@ class GroupMember < Member access_level end - def notify_create + private + + def send_invite + notification_service.invite_group_member(self, @raw_invite_token) + + super + end + + def post_create_hook notification_service.new_group_member(self) + + super end - def notify_update + def post_update_hook if access_level_changed? notification_service.update_group_member(self) end + + super end - def notification_service - NotificationService.new + def after_accept_invite + notification_service.accept_group_invite(self) + + super + end + + def after_decline_invite + notification_service.decline_group_invite(self) + + super end end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 30c09f768d7..1b0c76917aa 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -6,11 +6,15 @@ # access_level :integer not null # source_id :integer not null # source_type :string(255) not null -# user_id :integer 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 # class ProjectMember < Member @@ -27,10 +31,6 @@ class ProjectMember < Member validates_format_of :source_type, with: /\AProject\z/ default_scope { where(source_type: SOURCE_TYPE) } - after_create :post_create_hook - after_update :post_update_hook - after_destroy :post_destroy_hook - scope :in_project, ->(project) { where(source_id: project.id) } scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) } scope :with_user, ->(user) { where(user_id: user.id) } @@ -55,7 +55,7 @@ class ProjectMember < Member # :master # ) # - def add_users_into_projects(project_ids, user_ids, access) + def add_users_into_projects(project_ids, user_ids, access, current_user = nil) access_level = if roles_hash.has_key?(access) roles_hash[access] elsif roles_hash.values.include?(access.to_i) @@ -64,12 +64,14 @@ class ProjectMember < Member raise "Non valid access" end + users = user_ids.map { |user_id| Member.user_for_id(user_id) } + ProjectMember.transaction do project_ids.each do |project_id| - user_ids.each do |user_id| - member = ProjectMember.new(access_level: access_level, user_id: user_id) - member.source_id = project_id - member.save + project = Project.find(project_id) + + users.each do |user| + Member.add_user(project.project_members, user, access_level, current_user) end end end @@ -82,6 +84,7 @@ class ProjectMember < Member def truncate_teams(project_ids) ProjectMember.transaction do members = ProjectMember.where(source_id: project_ids) + members.each do |member| member.destroy end @@ -109,44 +112,58 @@ class ProjectMember < Member access_level end + def project + source + end + def owner? project.owner == user end + private + + def send_invite + notification_service.invite_project_member(self, @raw_invite_token) + + super + end + def post_create_hook - Event.create( - project_id: self.project.id, - action: Event::JOINED, - author_id: self.user.id - ) - - notification_service.new_team_member(self) unless owner? - system_hook_service.execute_hooks_for(self, :create) + unless owner? + event_service.join_project(self.project, self.user) + notification_service.new_project_member(self) + end + + super end def post_update_hook - notification_service.update_team_member(self) if self.access_level_changed? + if access_level_changed? + notification_service.update_project_member(self) + end + + super end def post_destroy_hook - Event.create( - project_id: self.project.id, - action: Event::LEFT, - author_id: self.user.id - ) + event_service.leave_project(self.project, self.user) - system_hook_service.execute_hooks_for(self, :destroy) + super end - def notification_service - NotificationService.new + def after_accept_invite + notification_service.accept_project_invite(self) + + super end - def system_hook_service - SystemHooksService.new + def after_decline_invite + notification_service.decline_project_invite(self) + + super end - def project - source + def event_service + EventCreateService.new end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7c525b02f48..487d62e65b6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -18,15 +18,18 @@ # iid :integer # description :text # position :integer default(0) +# locked_at :datetime # require Rails.root.join("app/models/commit") require Rails.root.join("lib/static_model") class MergeRequest < ActiveRecord::Base + include InternalId include Issuable + include Referable + include Sortable include Taskable - include InternalId belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" @@ -70,6 +73,16 @@ class MergeRequest < ActiveRecord::Base transition locked: :reopened end + after_transition any => :locked do |merge_request, transition| + merge_request.locked_at = Time.now + merge_request.save + end + + after_transition locked: (any - :locked) do |merge_request, transition| + merge_request.locked_at = nil + merge_request.save + end + state :opened state :reopened state :closed @@ -83,16 +96,25 @@ class MergeRequest < ActiveRecord::Base end event :mark_as_mergeable do - transition unchecked: :can_be_merged + transition [:unchecked, :cannot_be_merged] => :can_be_merged end event :mark_as_unmergeable do - transition unchecked: :cannot_be_merged + transition [:unchecked, :can_be_merged] => :cannot_be_merged end state :unchecked state :can_be_merged state :cannot_be_merged + + around_transition do |merge_request, transition, block| + merge_request.record_timestamps = false + begin + block.call + ensure + merge_request.record_timestamps = true + end + end end validates :source_project, presence: true, unless: :allow_broken @@ -103,7 +125,6 @@ class MergeRequest < ActiveRecord::Base validate :validate_fork scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.project_ids) } - scope :of_user_team, ->(team) { where("(source_project_id in (:team_project_ids) OR target_project_id in (:team_project_ids) AND assignee_id in (:team_member_ids))", team_project_ids: team.project_ids, team_member_ids: team.member_ids) } scope :merged, -> { with_state(:merged) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } @@ -113,7 +134,31 @@ class MergeRequest < ActiveRecord::Base # Closed scope for merge request should return # both merged and closed mr's scope :closed, -> { with_states(:closed, :merged) } - scope :declined, -> { with_states(:closed) } + scope :rejected, -> { with_states(:closed) } + + def self.reference_prefix + '!' + end + + # Pattern used to extract `!123` merge request references from text + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?<merge_request>\d+) + }x + end + + def to_reference(from_project = nil) + reference = "#{self.class.reference_prefix}#{iid}" + + if cross_project_reference?(from_project) + reference = project.to_reference + reference + end + + reference + end def validate_branches if target_project == source_project && target_branch == source_branch @@ -152,7 +197,6 @@ class MergeRequest < ActiveRecord::Base def update_merge_request_diff if source_branch_changed? || target_branch_changed? reload_code - mark_as_unchecked end end @@ -179,22 +223,45 @@ class MergeRequest < ActiveRecord::Base end def automerge!(current_user, commit_message = nil) - MergeRequests::AutoMergeService.new.execute(self, current_user, commit_message) + return unless automergeable? + + MergeRequests::AutoMergeService. + new(target_project, current_user). + execute(self, commit_message) end def open? opened? || reopened? end + def work_in_progress? + title =~ /\A\[?WIP\]?:? /i + end + + def automergeable? + open? && !work_in_progress? && can_be_merged? + end + + def automerge_status + if work_in_progress? + "work_in_progress" + else + merge_status_name + end + end + def mr_and_commit_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 commit_ids = commits.last(commits_for_notes_limit).map(&:id) - project.notes.where( - "(noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR (noteable_type = 'Commit' AND commit_id IN (:commit_ids))", + Note.where( + "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" + + "(project_id = :source_project_id AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))", mr_id: id, - commit_ids: commit_ids + commit_ids: commit_ids, + target_project_id: target_project_id, + source_project_id: source_project_id ) end @@ -220,7 +287,7 @@ class MergeRequest < ActiveRecord::Base } unless last_commit.nil? - attrs.merge!(last_commit: last_commit.hook_attrs(source_project)) + attrs.merge!(last_commit: last_commit.hook_attrs) end attributes.merge!(attrs) @@ -235,21 +302,17 @@ class MergeRequest < ActiveRecord::Base end # Return the set of issues that will be closed if this merge request is accepted. - def closes_issues + def closes_issues(current_user = self.author) if target_branch == project.default_branch - issues = commits.flat_map { |c| c.closes_issues(project) } - issues += Gitlab::ClosingIssueExtractor.closed_by_message_in_project(description, project) + issues = commits.flat_map { |c| c.closes_issues(current_user) } + issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user). + closed_by_message(description)) issues.uniq.sort_by(&:id) else [] end end - # Mentionable override. - def gfm_reference - "merge request !#{iid}" - end - def target_project_path if target_project target_project.path_with_namespace @@ -318,7 +381,7 @@ class MergeRequest < ActiveRecord::Base end # Return array of possible target branches - # dependes on target project of MR + # depends on target project of MR def target_branches if target_project.nil? [] @@ -328,7 +391,7 @@ class MergeRequest < ActiveRecord::Base end # Return array of possible source branches - # dependes on source project of MR + # depends on source project of MR def source_branches if source_project.nil? [] @@ -336,4 +399,22 @@ class MergeRequest < ActiveRecord::Base source_project.repository.branch_names end end + + def locked_long_ago? + return false unless locked? + + locked_at.nil? || locked_at < (Time.now - 1.day) + end + + def has_ci? + source_project.ci_service && commits.any? + end + + def branch_missing? + !source_branch_exists? || !target_branch_exists? + end + + def can_be_merged_by?(user) + ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch) + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index a71122d5e07..df1c2b78758 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -14,6 +14,8 @@ require Rails.root.join("app/models/commit") class MergeRequestDiff < ActiveRecord::Base + include Sortable + # Prevent store of diff # if commits amount more then 200 COMMITS_SAFE_SIZE = 200 @@ -65,7 +67,7 @@ class MergeRequestDiff < ActiveRecord::Base end def load_commits(array) - array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash)) } + array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) } end def dump_diffs(diffs) @@ -86,7 +88,7 @@ class MergeRequestDiff < ActiveRecord::Base commits = compare_result.commits if commits.present? - commits = Commit.decorate(commits). + commits = Commit.decorate(commits, merge_request.source_project). sort_by(&:created_at). reverse end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 8fd3e56d2ee..9c543b37023 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -15,6 +15,7 @@ class Milestone < ActiveRecord::Base include InternalId + include Sortable belongs_to :project has_many :issues @@ -65,7 +66,7 @@ class Milestone < ActiveRecord::Base def percent_complete ((closed_items_count * 100) / total_items_count).abs rescue ZeroDivisionError - 100 + 0 end def expires_at diff --git a/app/models/namespace.rb b/app/models/namespace.rb index ea4b48fdd7f..03d2ab165ea 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -14,21 +14,27 @@ # class Namespace < ActiveRecord::Base + include Sortable include Gitlab::ShellAdapter has_many :projects, dependent: :destroy belongs_to :owner, class_name: "User" validates :owner, presence: true, unless: ->(n) { n.type == "Group" } - validates :name, presence: true, uniqueness: true, - length: { within: 0..255 }, - format: { with: Gitlab::Regex.name_regex, - message: Gitlab::Regex.name_regex_message } + validates :name, + presence: true, uniqueness: true, + length: { within: 0..255 }, + format: { with: Gitlab::Regex.namespace_name_regex, + message: Gitlab::Regex.namespace_name_regex_message } + validates :description, length: { within: 0..255 } - validates :path, uniqueness: { case_sensitive: false }, presence: true, length: { within: 1..255 }, - exclusion: { in: Gitlab::Blacklist.path }, - format: { with: Gitlab::Regex.path_regex, - message: Gitlab::Regex.path_regex_message } + validates :path, + uniqueness: { case_sensitive: false }, + presence: true, + length: { within: 1..255 }, + exclusion: { in: Gitlab::Blacklist.path }, + format: { with: Gitlab::Regex.namespace_regex, + message: Gitlab::Regex.namespace_regex_message } delegate :name, to: :owner, allow_nil: true, prefix: true @@ -38,12 +44,46 @@ class Namespace < ActiveRecord::Base scope :root, -> { where('type IS NULL') } - def self.search(query) - where("name LIKE :query OR path LIKE :query", query: "%#{query}%") - end + class << self + def by_path(path) + where('lower(path) = :value', value: path.downcase).first + end + + # Case insensetive search for namespace by path or name + def find_by_path_or_name(path) + find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) + end + + def search(query) + where("name LIKE :query OR path LIKE :query", query: "%#{query}%") + end - def self.global_id - 'GLN' + def clean_path(path) + path = path.dup + # Get the email username by removing everything after an `@` sign. + path.gsub!(/@.*\z/, "") + # Usernames can't end in .git, so remove it. + path.gsub!(/\.git\z/, "") + # Remove dashes at the start of the username. + path.gsub!(/\A-+/, "") + # Remove periods at the end of the username. + path.gsub!(/\.+\z/, "") + # Remove everything that's not in the list of allowed characters. + path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + + # Users with the great usernames of "." or ".." would end up with a blank username. + # Work around that by setting their username to "blank", followed by a counter. + path = "blank" if path.blank? + + counter = 0 + base = path + while Namespace.find_by_path_or_name(path) + counter += 1 + path = "#{base}#{counter}" + end + + path + end end def to_param @@ -59,7 +99,18 @@ class Namespace < ActiveRecord::Base end def rm_dir - gitlab_shell.rm_namespace(path) + # Move namespace directory into trash. + # We will remove it later async + new_path = "#{path}+#{id}+deleted" + + if gitlab_shell.mv_namespace(path, new_path) + message = "Namespace directory \"#{path}\" moved to \"#{new_path}\"" + Gitlab::AppLogger.info message + + # Remove namespace directroy async with delay so + # GitLab has time to remove all projects first + GitlabShellWorker.perform_in(5.minutes, :rm_namespace, new_path) + end end def move_dir diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 43979b5e807..f4e90125373 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -84,7 +84,7 @@ module Network skip += self.class.max_count end else - # Cant't find the target commit in the repo. + # Can't find the target commit in the repo. offset = 0 end end @@ -226,7 +226,7 @@ module Network reserved = [] for day in time_range - reserved += @reserved[day] + reserved.push(*@reserved[day]) end reserved.uniq! diff --git a/app/models/note.rb b/app/models/note.rb index 5bf645bbd1d..68b9d433ae0 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -22,13 +22,16 @@ require 'file_size_validator' class Note < ActiveRecord::Base include Mentionable + include Gitlab::CurrentSettings + include Participable default_value_for :system, false attr_mentionable :note + participant :author, :mentioned_users belongs_to :project - belongs_to :noteable, polymorphic: true + belongs_to :noteable, polymorphic: true, touch: true belongs_to :author, class_name: "User" delegate :name, to: :project, prefix: true @@ -36,7 +39,8 @@ class Note < ActiveRecord::Base validates :note, :project, presence: true validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true - validates :attachment, file_size: { maximum: 10.megabytes.to_i } + # 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' } @@ -48,8 +52,9 @@ class Note < ActiveRecord::Base 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]) } - scope :fresh, ->{ order("created_at ASC, id ASC") } + scope :fresh, ->{ order(created_at: :asc, id: :asc) } scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author, ->{ includes(:author) } @@ -58,88 +63,6 @@ class Note < ActiveRecord::Base after_update :set_references class << self - def create_status_change_note(noteable, project, author, status, source) - body = "_Status changed to #{status}#{' by ' + source.gfm_reference if source}_" - - create( - noteable: noteable, - project: project, - author: author, - note: body, - system: true - ) - end - - # +noteable+ was referenced from +mentioner+, by including GFM in either - # +mentioner+'s description or an associated Note. - # Create a system Note associated with +noteable+ with a GFM back-reference - # to +mentioner+. - def create_cross_reference_note(noteable, mentioner, author, project) - gfm_reference = mentioner_gfm_ref(noteable, mentioner, project) - - note_options = { - project: project, - author: author, - note: cross_reference_note_content(gfm_reference), - system: true - } - - if noteable.kind_of?(Commit) - note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id) - else - note_options.merge!(noteable: noteable) - end - - create(note_options) unless cross_reference_disallowed?(noteable, mentioner) - end - - def create_milestone_change_note(noteable, project, author, milestone) - body = if milestone.nil? - '_Milestone removed_' - else - "_Milestone changed to #{milestone.title}_" - end - - create( - noteable: noteable, - project: project, - author: author, - note: body, - system: true - ) - end - - def create_assignee_change_note(noteable, project, author, assignee) - body = assignee.nil? ? '_Assignee removed_' : "_Reassigned to @#{assignee.username}_" - - create({ - noteable: noteable, - project: project, - author: author, - note: body, - system: true - }) - end - - def create_new_commits_note(noteable, project, author, commits) - commits_text = ActionController::Base.helpers.pluralize(commits.size, 'new commit') - body = "Added #{commits_text}:\n\n" - - commits.each do |commit| - message = "* #{commit.short_id} - #{commit.title}" - body << message - body << "\n" - end - - create( - noteable: noteable, - project: project, - author: author, - note: body, - system: true - ) - end - def discussions_from_notes(notes) discussion_ids = [] discussions = [] @@ -165,109 +88,17 @@ class Note < ActiveRecord::Base [:discussion, type.try(:underscore), id, line_code].join("-").to_sym end - # Determine if cross reference note should be created. - # eg. mentioning a commit in MR comments which exists inside a MR - # should not create "mentioned in" note. - def cross_reference_disallowed?(noteable, mentioner) - if mentioner.kind_of?(MergeRequest) - mentioner.commits.map(&:id).include? noteable.id - end - end - - # Determine whether or not a cross-reference note already exists. - def cross_reference_exists?(noteable, mentioner) - gfm_reference = mentioner_gfm_ref(noteable, mentioner) - notes = if noteable.is_a?(Commit) - where(commit_id: noteable.id) - else - where(noteable_id: noteable.id) - end - - notes.where('note like ?', cross_reference_note_content(gfm_reference)). - system.any? - end - def search(query) where("note like :query", query: "%#{query}%") end - - def cross_reference_note_prefix - '_mentioned in ' - end - - private - - def cross_reference_note_content(gfm_reference) - cross_reference_note_prefix + "#{gfm_reference}_" - end - - # Prepend the mentioner's namespaced project path to the GFM reference for - # cross-project references. For same-project references, return the - # unmodified GFM reference. - def mentioner_gfm_ref(noteable, mentioner, project = nil) - if mentioner.is_a?(Commit) - if project.nil? - return mentioner.gfm_reference.sub('commit ', 'commit %') - else - mentioning_project = project - end - else - mentioning_project = mentioner.project - end - - noteable_project_id = noteable_project_id(noteable, mentioning_project) - - full_gfm_reference(mentioning_project, noteable_project_id, mentioner) - end - - # Return the ID of the project that +noteable+ belongs to, or nil if - # +noteable+ is a commit and is not part of the project that owns - # +mentioner+. - def noteable_project_id(noteable, mentioning_project) - if noteable.is_a?(Commit) - if mentioning_project.repository.commit(noteable.id) - # The noteable commit belongs to the mentioner's project - mentioning_project.id - else - nil - end - else - noteable.project.id - end - end - - # Return the +mentioner+ GFM reference. If the mentioner and noteable - # projects are not the same, add the mentioning project's path to the - # returned value. - def full_gfm_reference(mentioning_project, noteable_project_id, mentioner) - if mentioning_project.id == noteable_project_id - mentioner.gfm_reference - else - if mentioner.is_a?(Commit) - mentioner.gfm_reference.sub( - /(commit )/, - "\\1#{mentioning_project.path_with_namespace}@" - ) - else - mentioner.gfm_reference.sub( - /(issue |merge request )/, - "\\1#{mentioning_project.path_with_namespace}" - ) - end - end - end end - def commit_author - @commit_author ||= - project.team.users.find_by(email: noteable.author_email) || - project.team.users.find_by(name: noteable.author_name) - rescue - nil + def cross_reference? + system && SystemNoteService.cross_reference?(note) end - def cross_reference? - note.start_with?(self.class.cross_reference_note_prefix) + def max_attachment_size + current_application_settings.max_attachment_size.megabytes.to_i end def find_diff @@ -278,10 +109,14 @@ class Note < ActiveRecord::Base end end + def hook_attrs + attributes + end + def set_diff # First lets find notes with same diff # before iterating over all mr diffs - diff = Note.where(noteable_id: self.noteable_id, noteable_type: self.noteable_type, line_code: self.line_code).last.try(:diff) + diff = diff_for_line_code unless for_merge_request? diff ||= find_diff self.st_diff = diff.to_hash if diff @@ -291,11 +126,16 @@ class Note < ActiveRecord::Base @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 such line of code exists in merge request diff # If exists - its active discussion # If not - its outdated diff def active? return true unless self.diff + return false unless noteable noteable.diffs.each do |mr_diff| next unless mr_diff.new_path == self.diff.new_path @@ -378,19 +218,19 @@ class Note < ActiveRecord::Base prev_lines = [] diff_lines.each do |line| - if generate_line_code(line) != self.line_code - if line.type == "match" - prev_lines.clear - prev_match_line = line - else - prev_lines.push(line) - prev_lines.shift if prev_lines.length >= max_number_of_lines - end + if line.type == "match" + prev_lines.clear + prev_match_line = line else prev_lines << line - return prev_lines + + 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 def diff_lines @@ -401,16 +241,6 @@ class Note < ActiveRecord::Base @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code) end - # Returns true if this is a downvote note, - # otherwise false is returned - def downvote? - votable? && (note.start_with?('-1') || - note.start_with?(':-1:') || - note.start_with?(':thumbsdown:') || - note.start_with?(':thumbs_down_sign:') - ) - end - def for_commit? noteable_type == "Commit" end @@ -435,10 +265,14 @@ class Note < ActiveRecord::Base for_merge_request? && for_diff_line? end + def for_project_snippet? + noteable_type == "Snippet" + end + # override to return commits, which are not active record def noteable if for_commit? - project.repository.commit(commit_id) + project.commit(commit_id) else super end @@ -448,14 +282,38 @@ class Note < ActiveRecord::Base nil end - # Returns true if this is an upvote note, - # otherwise false is returned + DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:) + + # Check if the note is a downvote + def downvote? + votable? && note.start_with?(*DOWNVOTES) + end + + UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:) + + # Check if the note is an upvote def upvote? - votable? && (note.start_with?('+1') || - note.start_with?(':+1:') || - note.start_with?(':thumbsup:') || - note.start_with?(':thumbs_up_sign:') - ) + votable? && note.start_with?(*UPVOTES) + end + + def superceded?(notes) + return false unless vote? + + notes.each do |note| + next if note == self + + if note.vote? && + self[:author_id] == note[:author_id] && + self[:created_at] <= note[:created_at] + return true + end + end + + false + end + + def vote? + upvote? || downvote? end def votable? @@ -463,8 +321,8 @@ class Note < ActiveRecord::Base end # Mentionable override. - def gfm_reference - noteable.gfm_reference + def gfm_reference(from_project = nil) + noteable.gfm_reference(from_project) end # Mentionable override. @@ -479,7 +337,7 @@ class Note < ActiveRecord::Base end # FIXME: Hack for polymorphic associations with STI - # For more information wisit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations + # For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations def noteable_type=(sType) super(sType.to_s.classify.constantize.base_class.to_s) end @@ -502,6 +360,6 @@ class Note < ActiveRecord::Base end def editable? - !system + !read_attribute(:system) end end diff --git a/app/models/notification.rb b/app/models/notification.rb index b0f8ed6a4ec..1395274173d 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -6,12 +6,13 @@ class Notification N_PARTICIPATING = 1 N_WATCH = 2 N_GLOBAL = 3 + N_MENTION = 4 attr_accessor :target class << self def notification_levels - [N_DISABLED, N_PARTICIPATING, N_WATCH] + [N_DISABLED, N_PARTICIPATING, N_WATCH, N_MENTION] end def options_with_labels @@ -19,12 +20,13 @@ class Notification disabled: N_DISABLED, participating: N_PARTICIPATING, watch: N_WATCH, + mention: N_MENTION, global: N_GLOBAL } end def project_notification_levels - [N_DISABLED, N_PARTICIPATING, N_WATCH, N_GLOBAL] + [N_DISABLED, N_PARTICIPATING, N_WATCH, N_GLOBAL, N_MENTION] end end @@ -48,6 +50,10 @@ class Notification target.notification_level == N_GLOBAL end + def mention? + target.notification_level == N_MENTION + end + def level target.notification_level end diff --git a/app/models/project.rb b/app/models/project.rb index daf4bdd0aad..b161cbe86b9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -24,12 +24,22 @@ # 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) +# avatar :string(255) # +require 'carrierwave/orm/activerecord' +require 'file_size_validator' + class Project < ActiveRecord::Base + include Gitlab::ConfigHelper include Gitlab::ShellAdapter include Gitlab::VisibilityLevel - include Gitlab::ConfigHelper + include Rails.application.routes.url_helpers + include Referable + include Sortable + extend Gitlab::ConfigHelper extend Enumerize @@ -41,14 +51,20 @@ class Project < ActiveRecord::Base default_value_for :wall_enabled, false default_value_for :snippets_enabled, gitlab_config_features.snippets + # set last_activity_at to the same as created_at + after_create :set_last_activity_at + def set_last_activity_at + update_column(:last_activity_at, self.created_at) + end + ActsAsTaggableOn.strict_case_match = true acts_as_taggable_on :tags attr_accessor :new_default_branch # Relations - belongs_to :creator, foreign_key: "creator_id", class_name: "User" - belongs_to :group, -> { where(type: Group) }, foreign_key: "namespace_id" + belongs_to :creator, foreign_key: 'creator_id', class_name: 'User' + belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id' belongs_to :namespace has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id' @@ -58,29 +74,39 @@ class Project < ActiveRecord::Base has_one :gitlab_ci_service, dependent: :destroy has_one :campfire_service, dependent: :destroy has_one :emails_on_push_service, dependent: :destroy + has_one :irker_service, dependent: :destroy has_one :pivotaltracker_service, dependent: :destroy has_one :hipchat_service, dependent: :destroy has_one :flowdock_service, dependent: :destroy has_one :assembla_service, dependent: :destroy + has_one :asana_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy has_one :slack_service, dependent: :destroy - has_one :buildbox_service, dependent: :destroy + has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy + has_one :teamcity_service, dependent: :destroy has_one :pushover_service, dependent: :destroy + has_one :jira_service, dependent: :destroy + has_one :redmine_service, dependent: :destroy + has_one :custom_issue_tracker_service, dependent: :destroy + has_one :gitlab_issue_tracker_service, dependent: :destroy + has_one :external_wiki_service, dependent: :destroy + has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" + has_one :forked_from_project, through: :forked_project_link # Merge Requests for target project should be removed with it - has_many :merge_requests, dependent: :destroy, foreign_key: "target_project_id" + has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' # Merge requests from source project should be kept when source project was removed - has_many :fork_merge_requests, foreign_key: "source_project_id", class_name: MergeRequest - has_many :issues, -> { order 'issues.state DESC, issues.created_at DESC' }, dependent: :destroy + has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest + has_many :issues, dependent: :destroy has_many :labels, dependent: :destroy has_many :services, dependent: :destroy has_many :events, dependent: :destroy has_many :milestones, dependent: :destroy has_many :notes, dependent: :destroy - has_many :snippets, dependent: :destroy, class_name: "ProjectSnippet" - has_many :hooks, dependent: :destroy, class_name: "ProjectHook" + has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet' + has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :protected_branches, dependent: :destroy has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember' has_many :users, through: :project_members @@ -89,73 +115,80 @@ class Project < ActiveRecord::Base has_many :users_star_projects, dependent: :destroy has_many :starrers, through: :users_star_projects, source: :user + has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" + delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true # Validations validates :creator, presence: true, on: :create validates :description, length: { maximum: 2000 }, allow_blank: true - validates :name, presence: true, length: { within: 0..255 }, - format: { with: Gitlab::Regex.project_name_regex, - message: Gitlab::Regex.project_regex_message } - validates :path, presence: true, length: { within: 0..255 }, - exclusion: { in: Gitlab::Blacklist.path }, - format: { with: Gitlab::Regex.path_regex, - message: Gitlab::Regex.path_regex_message } + validates :name, + presence: true, + length: { within: 0..255 }, + format: { with: Gitlab::Regex.project_name_regex, + message: Gitlab::Regex.project_name_regex_message } + validates :path, + presence: true, + length: { within: 0..255 }, + format: { with: Gitlab::Regex.project_path_regex, + message: Gitlab::Regex.project_path_regex_message } validates :issues_enabled, :merge_requests_enabled, :wiki_enabled, inclusion: { in: [true, false] } - validates :visibility_level, - exclusion: { in: gitlab_config.restricted_visibility_levels }, - if: -> { gitlab_config.restricted_visibility_levels.any? } validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true validates :namespace, presence: true validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id validates :import_url, - format: { with: URI::regexp(%w(git http https)), message: "should be a valid url" }, + format: { with: /\A#{URI.regexp(%w(ssh git http https))}\z/, message: 'should be a valid url' }, if: :import? validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create + validate :avatar_type, + if: ->(project) { project.avatar.present? && project.avatar_changed? } + validates :avatar, file_size: { maximum: 200.kilobytes.to_i } + + mount_uploader :avatar, AvatarUploader # Scopes - 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_team, ->(team) { where("projects.id IN (:ids)", ids: team.projects.map(&:id)) } - scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) } + 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 :sorted_by_activity, -> { reorder("projects.last_activity_at DESC") } - scope :sorted_by_stars, -> { reorder("projects.star_count DESC") } scope :personal, ->(user) { where(namespace_id: user.namespace_id) } - scope :joined, ->(user) { where("namespace_id != ?", user.namespace_id) } + scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) } scope :public_only, -> { where(visibility_level: Project::PUBLIC) } scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) } scope :non_archived, -> { where(archived: false) } - enumerize :issues_tracker, in: (Gitlab.config.issues_tracker.keys).append(:gitlab), default: :gitlab - state_machine :import_status, initial: :none do event :import_start do transition [:none, :finished] => :started end event :import_finish do - transition :started => :finished + transition started: :finished end event :import_fail do - transition :started => :failed + transition started: :failed end event :import_retry do - transition :failed => :started + transition failed: :started end state :started state :finished state :failed - after_transition any => :started, :do => :add_import_job + after_transition any => :started, do: :add_import_job + after_transition any => :finished, do: :clear_import_data end class << self @@ -169,7 +202,7 @@ class Project < ActiveRecord::Base def publicish(user) visibility_levels = [Project::PUBLIC] - visibility_levels += [Project::INTERNAL] if user + visibility_levels << Project::INTERNAL if user where(visibility_level: visibility_levels) end @@ -178,21 +211,26 @@ class Project < ActiveRecord::Base end def active - joins(:issues, :notes, :merge_requests).order("issues.created_at, notes.created_at, merge_requests.created_at DESC") + joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') end def search(query) - joins(:namespace).where("projects.archived = ?", false).where("projects.name LIKE :query OR projects.path LIKE :query OR namespaces.name LIKE :query OR projects.description LIKE :query", query: "%#{query}%") + joins(:namespace).where('projects.archived = ?', false). + where('LOWER(projects.name) LIKE :query OR + LOWER(projects.path) LIKE :query OR + LOWER(namespaces.name) LIKE :query OR + LOWER(projects.description) LIKE :query', + query: "%#{query.try(:downcase)}%") end def search_by_title(query) - where("projects.archived = ?", false).where("LOWER(projects.name) LIKE :query", query: "%#{query.downcase}%") + where('projects.archived = ?', false).where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%") end def find_with_namespace(id) - return nil unless id.include?("/") + return nil unless id.include?('/') - id = id.split("/") + id = id.split('/') namespace = Namespace.find_by(path: id.first) return nil unless namespace @@ -204,15 +242,17 @@ class Project < ActiveRecord::Base end def sort(method) - case method.to_s - when 'newest' then reorder('projects.created_at DESC') - when 'oldest' then reorder('projects.created_at ASC') - when 'recently_updated' then reorder('projects.updated_at DESC') - when 'last_updated' then reorder('projects.updated_at ASC') - when 'largest_repository' then reorder('projects.repository_size DESC') - else reorder("namespaces.path, projects.name ASC") + if method == 'repository_size_desc' + reorder(repository_size: :desc, id: :desc) + else + order_by(method) end end + + def reference_pattern + name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR + %r{(?<project>#{name_pattern}/#{name_pattern})} + end end def team @@ -220,7 +260,11 @@ class Project < ActiveRecord::Base end def repository - @repository ||= Repository.new(path_with_namespace) + @repository ||= Repository.new(path_with_namespace, nil, self) + end + + def commit(id = 'HEAD') + repository.commit(id) end def saved? @@ -231,6 +275,10 @@ class Project < ActiveRecord::Base RepositoryImportWorker.perform_in(2.seconds, id) end + def clear_import_data + self.import_data.destroy if self.import_data + end + def import? import_url.present? end @@ -260,19 +308,23 @@ class Project < ActiveRecord::Base end def to_param - namespace.path + "/" + path + path + end + + def to_reference(_from_project = nil) + path_with_namespace end def web_url - [gitlab_config.url, path_with_namespace].join("/") + [gitlab_config.url, path_with_namespace].join('/') end def web_url_without_protocol - web_url.split("://")[1] + web_url.split('://')[1] end def build_commit_note(commit) - notes.new(commit_id: commit.id, noteable_type: "Commit") + notes.new(commit_id: commit.id, noteable_type: 'Commit') end def last_activity @@ -287,34 +339,69 @@ class Project < ActiveRecord::Base self.id end + def get_issue(issue_id) + if default_issues_tracker? + issues.find_by(iid: issue_id) + else + ExternalIssue.new(issue_id, self) + end + end + def issue_exists?(issue_id) - if used_default_issues_tracker? - self.issues.where(iid: issue_id).first.present? + get_issue(issue_id) + end + + def default_issue_tracker + gitlab_issue_tracker_service || create_gitlab_issue_tracker_service + end + + def issues_tracker + if external_issue_tracker + external_issue_tracker else - true + default_issue_tracker end end - def used_default_issues_tracker? - self.issues_tracker == Project.issues_tracker.default_value + def default_issues_tracker? + !external_issue_tracker + end + + def external_issues_trackers + services.select(&:issue_tracker?).reject(&:default?) + end + + def external_issue_tracker + @external_issues_tracker ||= external_issues_trackers.select(&:activated?).first end def can_have_issues_tracker_id? - self.issues_enabled && !self.used_default_issues_tracker? + self.issues_enabled && !self.default_issues_tracker? end def build_missing_services - available_services_names.each do |service_name| - service = services.find { |service| service.to_param == service_name } + services_templates = Service.where(template: true) + + Service.available_services_names.each do |service_name| + service = find_service(services, service_name) # If service is available but missing in db - # we should create an instance. Ex `create_gitlab_ci_service` - service = self.send :"create_#{service_name}_service" if service.nil? + if service.nil? + # We should check if template for the service exists + template = find_service(services_templates, service_name) + + if template.nil? + # If no template, we should create an instance. Ex `create_gitlab_ci_service` + service = self.send :"create_#{service_name}_service" + else + Service.create_from_template(self.id, template) + end + end end end - def available_services_names - %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium slack pushover buildbox bamboo) + def find_service(list, name) + list.find { |service| service.to_param == name } end def gitlab_ci? @@ -329,6 +416,27 @@ class Project < ActiveRecord::Base @ci_service ||= ci_services.select(&:activated?).first end + def avatar_type + unless self.avatar.image? + self.errors.add :avatar, 'only images allowed' + end + end + + def avatar_in_git + @avatar_file ||= 'logo.png' if repository.blob_at_branch('master', 'logo.png') + @avatar_file ||= 'logo.jpg' if repository.blob_at_branch('master', 'logo.jpg') + @avatar_file ||= 'logo.gif' if repository.blob_at_branch('master', 'logo.gif') + @avatar_file + end + + def avatar_url + if avatar.present? + [gitlab_config.url, avatar.url].join + elsif avatar_in_git + [gitlab_config.url, namespace_project_avatar_path(namespace, self)].join + end + end + # For compatibility with old code def code path @@ -355,20 +463,20 @@ class Project < ActiveRecord::Base end end - def team_member_by_name_or_email(name = nil, email = nil) - user = users.where("name like ? or email like ?", name, email).first + def project_member_by_name_or_email(name = nil, email = nil) + user = users.where('name like ? or email like ?', name, email).first project_members.where(user: user) if user end # Get Team Member record by user id - def team_member_by_id(user_id) + def project_member_by_id(user_id) project_members.find_by(user_id: user_id) end def name_with_namespace @name_with_namespace ||= begin if namespace - namespace.human_name + " / " + name + namespace.human_name + ' / ' + name else name end @@ -385,19 +493,14 @@ class Project < ActiveRecord::Base def execute_hooks(data, hooks_scope = :push_hooks) hooks.send(hooks_scope).each do |hook| - hook.async_execute(data) + hook.async_execute(data, hooks_scope.to_s) end end - def execute_services(data) - services.each do |service| - - # Call service hook only if it is active - begin - service.execute(data) if service.active - rescue => e - logger.error(e) - end + def execute_services(data, hooks_scope = :push_hooks) + # Call only service hooks that are active for this scope + services.send(hooks_scope).each do |service| + service.async_execute(data) end end @@ -409,7 +512,7 @@ class Project < ActiveRecord::Base def valid_repo? repository.exists? rescue - errors.add(:path, "Invalid repository path") + errors.add(:path, 'Invalid repository path') false end @@ -468,7 +571,7 @@ class Project < ActiveRecord::Base end def http_url_to_repo - [gitlab_config.url, "/", path_with_namespace, ".git"].join('') + [gitlab_config.url, '/', path_with_namespace, '.git'].join('') end # Check if current branch name is marked as protected in the system @@ -476,6 +579,10 @@ class Project < ActiveRecord::Base protected_branches_names.include?(branch_name) end + def developers_can_push_to_protected_branch?(branch_name) + protected_branches.any? { |pb| pb.name == branch_name && pb.developers_can_push } + end + def forked? !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?) end @@ -527,6 +634,7 @@ class Project < ActiveRecord::Base # Since we do cache @event we need to reset cache in special cases: # * when project was moved # * when project was renamed + # * when the project avatar changes # Events cache stored like events/23-20130109142513. # The cache key includes updated_at timestamp. # Thus it will automatically generate a new fragment @@ -588,11 +696,21 @@ class Project < ActiveRecord::Base end def create_repository - if gitlab_shell.add_repository(path_with_namespace) - true + if forked? + if gitlab_shell.fork_repository(forked_from_project.path_with_namespace, self.namespace.path) + ensure_satellite_exists + true + else + errors.add(:base, 'Failed to fork repository') + false + end else - errors.add(:base, "Failed to create repository") - false + if gitlab_shell.add_repository(path_with_namespace) + true + else + errors.add(:base, 'Failed to create repository') + false + end end end @@ -604,7 +722,7 @@ class Project < ActiveRecord::Base ProjectWiki.new(self, self.owner).wiki true rescue ProjectWiki::CouldNotCreateWikiError => ex - errors.add(:base, "Failed create wiki") + errors.add(:base, 'Failed create wiki') false end end diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb new file mode 100644 index 00000000000..cd3319f077e --- /dev/null +++ b/app/models/project_import_data.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: project_import_data +# +# id :integer not null, primary key +# project_id :integer +# data :text +# + +require 'carrierwave/orm/activerecord' +require 'file_size_validator' + +class ProjectImportData < ActiveRecord::Base + belongs_to :project + + serialize :data, JSON + + validates :project, presence: true +end diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb new file mode 100644 index 00000000000..e6e16058d41 --- /dev/null +++ b/app/models/project_services/asana_service.rb @@ -0,0 +1,127 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# +require 'asana' + +class AsanaService < Service + prop_accessor :api_key, :restrict_to_branch + validates :api_key, presence: true, if: :activated? + + def title + 'Asana' + end + + def description + 'Asana - Teamwork without email' + end + + def help + 'This service adds commit messages as comments to Asana tasks. +Once enabled, commit messages are checked for Asana task URLs +(for example, `https://app.asana.com/0/123456/987654`) or task IDs +starting with # (for example, `#987654`). Every task ID found will +get the commit comment added to it. + +You can also close a task with a message containing: `fix #123456`. + +You can find your Api Keys here: +http://developer.asana.com/documentation/#api_keys' + end + + def to_param + 'asana' + end + + def fields + [ + { + type: 'text', + name: 'api_key', + placeholder: 'User API token. User must have access to task, +all comments will be attributed to this user.' + }, + { + type: 'text', + name: 'restrict_to_branch', + placeholder: 'Comma-separated list of branches which will be +automatically inspected. Leave blank to include all branches.' + } + ] + end + + def supported_events + %w(push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + Asana.configure do |client| + client.api_key = api_key + end + + user = data[:user_name] + branch = Gitlab::Git.ref_name(data[:ref]) + + branch_restriction = restrict_to_branch.to_s + + # check the branch restriction is poplulated and branch is not included + if branch_restriction.length > 0 && branch_restriction.index(branch).nil? + return + end + + project_name = project.name_with_namespace + push_msg = user + ' pushed to branch ' + branch + ' of ' + project_name + + data[:commits].each do |commit| + check_commit(' ( ' + commit[:url] + ' ): ' + commit[:message], push_msg) + end + end + + def check_commit(message, push_msg) + task_list = [] + close_list = [] + + message.split("\n").each do |line| + # look for a task ID or a full Asana url + task_list.concat(line.scan(/#(\d+)/)) + task_list.concat(line.scan(/https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)/)) + # look for a word starting with 'fix' followed by a task ID + close_list.concat(line.scan(/(fix\w*)\W*#(\d+)/i)) + end + + # post commit to every taskid found + task_list.each do |taskid| + task = Asana::Task.find(taskid[0]) + + if task + task.create_story(text: push_msg + ' ' + message) + end + end + + # close all tasks that had 'fix(ed/es/ing) #:id' in them + close_list.each do |taskid| + task = Asana::Task.find(taskid.last) + + if task + task.modify(completed: true) + end + end + end +end diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index 0b90a14f39c..fb7e0c0fb0d 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -2,14 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# 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 # class AssemblaService < Service @@ -37,8 +43,14 @@ class AssemblaService < Service ] end - def execute(push) + def supported_events + %w(push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}" - AssemblaService.post(url, body: { payload: push }.to_json, headers: { 'Content-Type' => 'application/json' }) + AssemblaService.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' }) end end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index b9eec9ab21e..d8aedbd2ab4 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -1,15 +1,41 @@ +# == 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 +# + class BambooService < CiService include HTTParty prop_accessor :bamboo_url, :build_key, :username, :password - validates :bamboo_url, presence: true, - format: { with: URI::regexp }, if: :activated? + validates :bamboo_url, + presence: true, + format: { with: /\A#{URI.regexp}\z/ }, + if: :activated? validates :build_key, presence: true, if: :activated? - validates :username, presence: true, - if: ->(service) { service.password? }, if: :activated? - validates :password, presence: true, - if: ->(service) { service.username? }, if: :activated? + validates :username, + presence: true, + if: ->(service) { service.password? }, + if: :activated? + validates :password, + presence: true, + if: ->(service) { service.username? }, + if: :activated? attr_accessor :response @@ -48,6 +74,10 @@ class BambooService < CiService ] end + def supported_events + %w(push) + end + def build_info(sha) url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}") @@ -63,7 +93,7 @@ class BambooService < CiService end end - def build_page(sha) + def build_page(sha, ref) build_info(sha) if @response.nil? || !@response.code if @response.code != 200 || @response['results']['results']['size'] == '0' @@ -76,7 +106,7 @@ class BambooService < CiService end end - def commit_status(sha) + def commit_status(sha, ref) build_info(sha) if @response.nil? || !@response.code return :error unless @response.code == 200 || @response.code == 404 @@ -97,7 +127,9 @@ class BambooService < CiService end end - def execute(_data) + def execute(data) + return unless supported_events.include?(data[:object_kind]) + # Bamboo requires a GET and does not take any data. self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}", verify: false) diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildkite_service.rb index 0ab67b79fe4..a714bc82246 100644 --- a/app/models/project_services/buildbox_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -2,19 +2,27 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # require "addressable/uri" -class BuildboxService < CiService +class BuildkiteService < CiService + ENDPOINT = "https://buildkite.com" + prop_accessor :project_url, :token validates :project_url, presence: true, if: :activated? @@ -23,7 +31,7 @@ class BuildboxService < CiService after_save :compose_service_hook, if: :activated? def webhook_url - "#{buildbox_endpoint('webhook')}/deliver/#{webhook_token}" + "#{buildkite_endpoint('webhook')}/deliver/#{webhook_token}" end def compose_service_hook @@ -32,11 +40,17 @@ class BuildboxService < CiService hook.save end + def supported_events + %w(push) + end + def execute(data) + return unless supported_events.include?(data[:object_kind]) + service_hook.execute(data) end - def commit_status(sha) + def commit_status(sha, ref) response = HTTParty.get(commit_status_path(sha), verify: false) if response.code == 200 && response['status'] @@ -47,10 +61,10 @@ class BuildboxService < CiService end def commit_status_path(sha) - "#{buildbox_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}" + "#{buildkite_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}" end - def build_page(sha) + def build_page(sha, ref) "#{project_url}/builds?commit=#{sha}" end @@ -59,11 +73,11 @@ class BuildboxService < CiService end def status_img_path - "#{buildbox_endpoint('badge')}/#{status_token}.svg" + "#{buildkite_endpoint('badge')}/#{status_token}.svg" end def title - 'Buildbox' + 'Buildkite' end def description @@ -71,18 +85,18 @@ class BuildboxService < CiService end def to_param - 'buildbox' + 'buildkite' end def fields [ { type: 'text', name: 'token', - placeholder: 'Buildbox project GitLab token' }, + placeholder: 'Buildkite project GitLab token' }, { type: 'text', name: 'project_url', - placeholder: 'https://buildbox.io/example/project' } + placeholder: "#{ENDPOINT}/example/project" } ] end @@ -104,11 +118,9 @@ class BuildboxService < CiService end end - def buildbox_endpoint(subdomain = nil) - endpoint = 'https://buildbox.io' - + def buildkite_endpoint(subdomain = nil) if subdomain.present? - uri = Addressable::URI.parse(endpoint) + uri = Addressable::URI.parse(ENDPOINT) new_endpoint = "#{uri.scheme || 'http'}://#{subdomain}.#{uri.host}" if uri.port.present? @@ -117,7 +129,7 @@ class BuildboxService < CiService new_endpoint end else - endpoint + ENDPOINT end end end diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 0736ddab99b..e591afdda64 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -2,14 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# 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 # class CampfireService < Service @@ -36,11 +42,17 @@ class CampfireService < Service ] end - def execute(push_data) + def supported_events + %w(push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + room = gate.find_room_by_name(self.room) return true unless room - message = build_message(push_data) + message = build_message(data) room.speak(message) end @@ -52,7 +64,7 @@ class CampfireService < Service end def build_message(push) - ref = push[:ref].gsub("refs/heads/", "") + ref = Gitlab::Git.ref_name(push[:ref]) before = push[:before] after = push[:after] @@ -60,9 +72,9 @@ class CampfireService < Service message << "[#{project.name_with_namespace}] " message << "#{push[:user_name]} " - if before =~ /000000/ + if Gitlab::Git.blank_ref?(before) message << "pushed new branch #{ref} \n" - elsif after =~ /000000/ + elsif Gitlab::Git.blank_ref?(after) message << "removed branch #{ref} \n" else message << "pushed #{push[:total_commits_count]} commits to #{ref}. " diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index b1d5e49ede3..77d48d4af5e 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -2,14 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# 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 # # Base class for CI services @@ -20,12 +26,16 @@ class CiService < Service :ci end + def supported_events + %w(push) + end + # Return complete url to build page # # Ex. # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c # - def build_page(sha) + def build_page(sha, ref) # implement inside child end @@ -42,7 +52,7 @@ class CiService < Service # # => 'running' # # - def commit_status(sha) + def commit_status(sha, ref) # implement inside child end end diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb new file mode 100644 index 00000000000..7c2027c18e6 --- /dev/null +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -0,0 +1,58 @@ +# == 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 +# + +class CustomIssueTrackerService < IssueTrackerService + + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + + def title + if self.properties && self.properties['title'].present? + self.properties['title'] + else + 'Custom Issue Tracker' + end + end + + def description + if self.properties && self.properties['description'].present? + self.properties['description'] + else + 'Custom issue tracker' + end + end + + def to_param + 'custom_issue_tracker' + end + + def fields + [ + { type: 'text', name: 'title', placeholder: title }, + { type: 'text', name: 'description', placeholder: description }, + { type: 'text', name: 'project_url', placeholder: 'Project url' }, + { type: 'text', name: 'issues_url', placeholder: 'Issue url' }, + { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' } + ] + end + + def initialize_properties + self.properties = {} if properties.nil? + end +end diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index b9071b98295..8f5d8b086eb 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -2,17 +2,25 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# 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 # class EmailsOnPushService < Service + prop_accessor :send_from_committer_email + prop_accessor :disable_diffs prop_accessor :recipients validates :recipients, presence: true, if: :activated? @@ -28,12 +36,37 @@ class EmailsOnPushService < Service 'emails_on_push' end + def supported_events + %w(push tag_push) + end + def execute(push_data) - EmailsOnPushWorker.perform_async(project_id, recipients, push_data) + return unless supported_events.include?(push_data[:object_kind]) + + EmailsOnPushWorker.perform_async( + project_id, + recipients, + push_data, + send_from_committer_email: send_from_committer_email?, + disable_diffs: disable_diffs? + ) + end + + def send_from_committer_email? + self.send_from_committer_email == "1" + end + + def disable_diffs? + self.disable_diffs == "1" end def fields + domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ") [ + { type: 'checkbox', name: 'send_from_committer_email', title: "Send from committer", + help: "Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. #{domains})." }, + { type: 'checkbox', name: 'disable_diffs', title: "Disable code diffs", + help: "Don't include possibly sensitive code diffs in notification body." }, { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' }, ] end diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb new file mode 100644 index 00000000000..9c46af7e721 --- /dev/null +++ b/app/models/project_services/external_wiki_service.rb @@ -0,0 +1,54 @@ +# == 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 +# + +class ExternalWikiService < Service + include HTTParty + + prop_accessor :external_wiki_url + validates :external_wiki_url, + presence: true, + format: { with: /\A#{URI.regexp}\z/ }, + if: :activated? + + def title + 'External Wiki' + end + + def description + 'Replaces the link to the internal wiki with a link to an external wiki.' + end + + def to_param + 'external_wiki' + end + + def fields + [ + { type: 'text', name: 'external_wiki_url', placeholder: 'The URL of the external Wiki' }, + ] + end + + def execute(_data) + @response = HTTParty.get(properties['external_wiki_url'], verify: true) rescue nil + if @response !=200 + nil + end + end +end diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 86705f5dabd..bf801ba61ad 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -2,14 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # require "flowdock-git-hook" @@ -36,11 +42,17 @@ class FlowdockService < Service ] end - def execute(push_data) + def supported_events + %w(push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + Flowdock::Git.post( - push_data[:ref], - push_data[:before], - push_data[:after], + data[:ref], + data[:before], + data[:after], token: token, repo: project.repository.path_to_repo, repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}", diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 18fdd204ecd..91ef267ad79 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -2,14 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # require "gemnasium/gitlab_service" @@ -37,11 +43,17 @@ class GemnasiumService < Service ] end - def execute(push_data) + def supported_events + %w(push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + Gemnasium::GitlabService.execute( - ref: push_data[:ref], - before: push_data[:before], - after: push_data[:after], + ref: data[:ref], + before: data[:before], + after: data[:after], token: token, api_key: api_key, repo: project.repository.path_to_repo diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index fadebf968bc..19b5859d5c9 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -2,23 +2,29 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# 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 # class GitlabCiService < CiService + API_PREFIX = "api/v1" + prop_accessor :project_url, :token validates :project_url, presence: true, if: :activated? validates :token, presence: true, if: :activated? - delegate :execute, to: :service_hook, prefix: nil - after_save :compose_service_hook, if: :activated? def compose_service_hook @@ -27,17 +33,37 @@ class GitlabCiService < CiService hook.save end - def commit_status_path(sha) - project_url + "/commits/#{sha}/status.json?token=#{token}" + def supported_events + %w(push tag_push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + sha = data[:checkout_sha] + + if sha.present? + file = ci_yaml_file(sha) + + if file && file.data + data.merge!(ci_yaml_file: file.data) + end + end + + service_hook.execute(data) + end + + def commit_status_path(sha, ref) + URI::encode(project_url + "/refs/#{ref}/commits/#{sha}/status.json?token=#{token}") end - def get_ci_build(sha) + def get_ci_build(sha, ref) @ci_builds ||= {} - @ci_builds[sha] ||= HTTParty.get(commit_status_path(sha), verify: false) + @ci_builds[sha] ||= HTTParty.get(commit_status_path(sha, ref), verify: false) end - def commit_status(sha) - response = get_ci_build(sha) + def commit_status(sha, ref) + response = get_ci_build(sha, ref) if response.code == 200 and response["status"] response["status"] @@ -46,16 +72,36 @@ class GitlabCiService < CiService end end - def commit_coverage(sha) - response = get_ci_build(sha) + def fork_registration(new_project, private_token) + params = { + id: new_project.id, + name_with_namespace: new_project.name_with_namespace, + web_url: new_project.web_url, + default_branch: new_project.default_branch, + ssh_url_to_repo: new_project.ssh_url_to_repo + } + + HTTParty.post( + fork_registration_path, + body: { + project_id: project.id, + project_token: token, + private_token: private_token, + data: params }, + verify: false + ) + end + + def commit_coverage(sha, ref) + response = get_ci_build(sha, ref) if response.code == 200 and response["coverage"] response["coverage"] end end - def build_page(sha) - project_url + "/commits/#{sha}" + def build_page(sha, ref) + URI::encode(project_url + "/refs/#{ref}/commits/#{sha}") end def builds_path @@ -81,7 +127,21 @@ class GitlabCiService < CiService def fields [ { type: 'text', name: 'token', placeholder: 'GitLab CI project specific token' }, - { type: 'text', name: 'project_url', placeholder: 'http://ci.gitlabhq.com/projects/3'} + { type: 'text', name: 'project_url', placeholder: 'http://ci.gitlabhq.com/projects/3' } ] end + + private + + def ci_yaml_file(sha) + repository.blob_at(sha, '.gitlab-ci.yml') + end + + def fork_registration_path + project_url.sub(/projects\/\d*/, "#{API_PREFIX}/forks") + end + + def repository + project.repository + end end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb new file mode 100644 index 00000000000..0ebc0a3ba1a --- /dev/null +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -0,0 +1,57 @@ +# == 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 +# + +class GitlabIssueTrackerService < IssueTrackerService + include Rails.application.routes.url_helpers + + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + + def default? + true + end + + def to_param + 'gitlab' + end + + def project_url + namespace_project_issues_url(project.namespace, project) + end + + def new_issue_url + new_namespace_project_issue_url(namespace_id: project.namespace, project_id: project) + end + + def issue_url(iid) + namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: iid) + end + + def project_path + namespace_project_issues_path(project.namespace, project) + end + + def new_issue_path + new_namespace_project_issue_path(namespace_id: project.namespace, project_id: project) + end + + def issue_path(iid) + namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: iid) + end +end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index a848d74044c..6761f00183e 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -2,20 +2,26 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# 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 # class HipchatService < Service MAX_COMMITS = 3 - prop_accessor :token, :room, :server + prop_accessor :token, :room, :server, :notify, :color, :api_version validates :token, presence: true, if: :activated? def title @@ -32,41 +38,73 @@ class HipchatService < Service def fields [ - { type: 'text', name: 'token', placeholder: '' }, - { type: 'text', name: 'room', placeholder: '' }, + { type: 'text', name: 'token', placeholder: 'Room token' }, + { type: 'text', name: 'room', placeholder: 'Room name or ID' }, + { type: 'checkbox', name: 'notify' }, + { type: 'select', name: 'color', choices: ['yellow', 'red', 'green', 'purple', 'gray', 'random'] }, + { type: 'text', name: 'api_version', + placeholder: 'Leave blank for default (v2)' }, { type: 'text', name: 'server', - placeholder: 'Leave blank for default. https://chat.hipchat.com' } + placeholder: 'Leave blank for default. https://hipchat.example.com' } ] end - def execute(push_data) - gate[room].send('GitLab', create_message(push_data)) + def supported_events + %w(push issue merge_request note tag_push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + message = create_message(data) + return unless message.present? + gate[room].send('GitLab', message, message_options) end private def gate - options = { api_version: 'v2' } - options[:server_url] = server unless server.nil? + options = { api_version: api_version.present? ? api_version : 'v2' } + options[:server_url] = server unless server.blank? @gate ||= HipChat::Client.new(token, options) end - def create_message(push) - ref = push[:ref].gsub("refs/heads/", "") + def message_options + { notify: notify.present? && notify == '1', color: color || 'yellow' } + end + + def create_message(data) + object_kind = data[:object_kind] + + message = \ + case object_kind + when "push", "tag_push" + create_push_message(data) + when "issue" + create_issue_message(data) unless is_update?(data) + when "merge_request" + create_merge_request_message(data) unless is_update?(data) + when "note" + create_note_message(data) + end + end + + def create_push_message(push) + ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch' + ref = Gitlab::Git.ref_name(push[:ref]) + before = push[:before] after = push[:after] message = "" message << "#{push[:user_name]} " - if before =~ /000000/ - message << "pushed new branch <a href=\""\ - "#{project.web_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"\ - " to <a href=\"#{project.web_url}\">"\ - "#{project.name_with_namespace.gsub!(/\s/, "")}</a>\n" - elsif after =~ /000000/ - message << "removed branch #{ref} from <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> \n" + if Gitlab::Git.blank_ref?(before) + message << "pushed new #{ref_type} <a href=\""\ + "#{project_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"\ + " to #{project_link}\n" + elsif Gitlab::Git.blank_ref?(after) + message << "removed #{ref_type} <b>#{ref}</b> from <a href=\"#{project.web_url}\">#{project_name}</a> \n" else - message << "pushed to branch <a href=\""\ + message << "pushed to #{ref_type} <a href=\""\ "#{project.web_url}/commits/#{URI.escape(ref)}\">#{ref}</a> " message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> " message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)" @@ -82,4 +120,129 @@ class HipchatService < Service message end + + def format_body(body) + if body + body = body.truncate(200, separator: ' ', omission: '...') + end + + "<pre>#{body}</pre>" + end + + def create_issue_message(data) + user_name = data[:user][:name] + + obj_attr = data[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + title = obj_attr[:title] + state = obj_attr[:state] + issue_iid = obj_attr[:iid] + issue_url = obj_attr[:url] + description = obj_attr[:description] + + issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>" + message = "#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>" + + if description + description = format_body(description) + message << description + end + + message + end + + def create_merge_request_message(data) + user_name = data[:user][:name] + + obj_attr = data[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + merge_request_id = obj_attr[:iid] + source_branch = obj_attr[:source_branch] + target_branch = obj_attr[:target_branch] + state = obj_attr[:state] + description = obj_attr[:description] + title = obj_attr[:title] + + merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" + merge_request_link = "<a href=\"#{merge_request_url}\">merge request ##{merge_request_id}</a>" + message = "#{user_name} #{state} #{merge_request_link} in " \ + "#{project_link}: <b>#{title}</b>" + + if description + description = format_body(description) + message << description + end + + message + end + + def format_title(title) + "<b>" + title.lines.first.chomp + "</b>" + end + + def create_note_message(data) + data = HashWithIndifferentAccess.new(data) + user_name = data[:user][:name] + + repo_attr = HashWithIndifferentAccess.new(data[:repository]) + + obj_attr = HashWithIndifferentAccess.new(data[:object_attributes]) + note = obj_attr[:note] + note_url = obj_attr[:url] + noteable_type = obj_attr[:noteable_type] + + case noteable_type + when "Commit" + commit_attr = HashWithIndifferentAccess.new(data[:commit]) + subject_desc = commit_attr[:id] + subject_desc = Commit.truncate_sha(subject_desc) + subject_type = "commit" + title = format_title(commit_attr[:message]) + when "Issue" + subj_attr = HashWithIndifferentAccess.new(data[:issue]) + subject_id = subj_attr[:iid] + subject_desc = "##{subject_id}" + subject_type = "issue" + title = format_title(subj_attr[:title]) + when "MergeRequest" + subj_attr = HashWithIndifferentAccess.new(data[:merge_request]) + subject_id = subj_attr[:iid] + subject_desc = "##{subject_id}" + subject_type = "merge request" + title = format_title(subj_attr[:title]) + when "Snippet" + subj_attr = HashWithIndifferentAccess.new(data[:snippet]) + subject_id = subj_attr[:id] + subject_desc = "##{subject_id}" + subject_type = "snippet" + title = format_title(subj_attr[:title]) + end + + subject_html = "<a href=\"#{note_url}\">#{subject_type} #{subject_desc}</a>" + message = "#{user_name} commented on #{subject_html} in #{project_link}: " + message << title + + if note + note = format_body(note) + message << note + end + + message + end + + def project_name + project.name_with_namespace.gsub(/\s/, '') + end + + def project_url + project.web_url + end + + def project_link + "<a href=\"#{project_url}\">#{project_name}</a>" + end + + def is_update?(data) + data[:object_attributes][:action] == 'update' + end end diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb new file mode 100644 index 00000000000..89f312e8c98 --- /dev/null +++ b/app/models/project_services/irker_service.rb @@ -0,0 +1,164 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'uri' + +class IrkerService < Service + prop_accessor :colorize_messages, :recipients, :channels + validates :recipients, presence: true, if: :activated? + validate :check_recipients_count, if: :activated? + + before_validation :get_channels + after_initialize :initialize_settings + + # Writer for RSpec tests + attr_writer :settings + + def initialize_settings + # See the documentation (doc/project_services/irker.md) for possible values + # here + @settings ||= { + server_ip: 'localhost', + server_port: 6659, + max_channels: 3, + default_irc_uri: nil + } + end + + def title + 'Irker (IRC gateway)' + end + + def description + 'Send IRC messages, on update, to a list of recipients through an Irker '\ + 'gateway.' + end + + def help + msg = 'Recipients have to be specified with a full URI: '\ + 'irc[s]://irc.network.net[:port]/#channel. Special cases: if you want '\ + 'the channel to be a nickname instead, append ",isnick" to the channel '\ + 'name; if the channel is protected by a secret password, append '\ + '"?key=secretpassword" to the URI.' + + unless @settings[:default_irc].nil? + msg += ' Note that a default IRC URI is provided by this service\'s '\ + "administrator: #{default_irc}. You can thus just give a channel name." + end + msg + end + + def to_param + 'irker' + end + + def supported_events + %w(push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + IrkerWorker.perform_async(project_id, channels, + colorize_messages, data, @settings) + end + + def fields + [ + { type: 'textarea', name: 'recipients', + placeholder: 'Recipients/channels separated by whitespaces' }, + { type: 'checkbox', name: 'colorize_messages' }, + ] + end + + private + + def check_recipients_count + return true if recipients.nil? || recipients.empty? + + if recipients.split(/\s+/).count > max_chans + errors.add(:recipients, "are limited to #{max_chans}") + end + end + + def max_chans + @settings[:max_channels] + end + + def get_channels + return true unless :activated? + return true if recipients.nil? || recipients.empty? + + map_recipients + + errors.add(:recipients, 'are all invalid') if channels.empty? + true + end + + def map_recipients + self.channels = recipients.split(/\s+/).map do |recipient| + format_channel default_irc_uri, recipient + end + channels.reject! &:nil? + end + + def default_irc_uri + default_irc = @settings[:default_irc_uri] + if !(default_irc.nil? || default_irc[-1] == '/') + default_irc += '/' + end + default_irc + end + + def format_channel(default_irc, recipient) + cnt = 0 + url = nil + + # Try to parse the chan as a full URI + begin + uri = URI.parse(recipient) + raise URI::InvalidURIError if uri.scheme.nil? && cnt == 0 + rescue URI::InvalidURIError + unless default_irc.nil? + cnt += 1 + recipient = "#{default_irc}#{recipient}" + retry if cnt == 1 + end + else + url = consider_uri uri + end + url + end + + def consider_uri(uri) + # Authorize both irc://domain.com/#chan and irc://domain.com/chan + if uri.is_a?(URI) && uri.scheme[/^ircs?\z/] && !uri.path.nil? + # Do not authorize irc://domain.com/ + if uri.fragment.nil? && uri.path.length > 1 + uri.to_s + else + # Authorize irc://domain.com/smthg#chan + # The irker daemon will deal with it by concatenating smthg and + # chan, thus sending messages on #smthgchan + uri.to_s + end + end + end +end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb new file mode 100644 index 00000000000..936e574cccd --- /dev/null +++ b/app/models/project_services/issue_tracker_service.rb @@ -0,0 +1,120 @@ +# == 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 +# + +class IssueTrackerService < Service + + validates :project_url, :issues_url, :new_issue_url, presence: true, if: :activated? + + def category + :issue_tracker + end + + def default? + false + end + + def issue_url(iid) + self.issues_url.gsub(':id', iid.to_s) + end + + def project_path + project_url + end + + def new_issue_path + new_issue_url + end + + def issue_path(iid) + issue_url(iid) + end + + def fields + [ + { type: 'text', name: 'description', placeholder: description }, + { type: 'text', name: 'project_url', placeholder: 'Project url' }, + { type: 'text', name: 'issues_url', placeholder: 'Issue url' }, + { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' } + ] + end + + def initialize_properties + if properties.nil? + if enabled_in_gitlab_config + self.properties = { + title: issues_tracker['title'], + project_url: add_issues_tracker_id(issues_tracker['project_url']), + issues_url: add_issues_tracker_id(issues_tracker['issues_url']), + new_issue_url: add_issues_tracker_id(issues_tracker['new_issue_url']) + } + else + self.properties = {} + end + end + end + + def supported_events + %w(push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + message = "#{self.type} was unable to reach #{self.project_url}. Check the url and try again." + result = false + + begin + response = HTTParty.head(self.project_url, verify: true) + + if response + message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}" + result = true + end + rescue HTTParty::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED => error + message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}" + end + Rails.logger.info(message) + result + end + + private + + def enabled_in_gitlab_config + Gitlab.config.issues_tracker && + Gitlab.config.issues_tracker.values.any? && + issues_tracker + end + + def issues_tracker + Gitlab.config.issues_tracker[to_param] + end + + def add_issues_tracker_id(url) + if self.project + id = self.project.issues_tracker_id + + if id + url = url.gsub(":issues_tracker_id", id) + end + end + + url + end +end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb new file mode 100644 index 00000000000..bfa8fc7b860 --- /dev/null +++ b/app/models/project_services/jira_service.rb @@ -0,0 +1,57 @@ +# == 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 +# + +class JiraService < IssueTrackerService + include Rails.application.routes.url_helpers + + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + + def help + line1 = '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.' + + line2 = '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)' + + [line1, line2].join("\n\n") + end + + def title + if self.properties && self.properties['title'].present? + self.properties['title'] + else + 'JIRA' + end + end + + def description + if self.properties && self.properties['description'].present? + self.properties['description'] + else + 'Jira issue tracker' + end + end + + def to_param + 'jira' + end +end diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 09e114f9cca..ade9ee97873 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -2,14 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# 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 # class PivotaltrackerService < Service @@ -36,9 +42,15 @@ class PivotaltrackerService < Service ] end - def execute(push) + def supported_events + %w(push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + url = 'https://www.pivotaltracker.com/services/v5/source_commits' - push[:commits].each do |commit| + data[:commits].each do |commit| message = { 'source_commit' => { 'commit_id' => commit[:id], diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index f247fde7762..53edf522e9a 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -2,14 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# 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 # class PushoverService < Service @@ -75,21 +81,27 @@ class PushoverService < Service ] end - def execute(push_data) - ref = push_data[:ref].gsub('refs/heads/', '') - before = push_data[:before] - after = push_data[:after] + def supported_events + %w(push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + ref = Gitlab::Git.ref_name(data[:ref]) + before = data[:before] + after = data[:after] - if before =~ /000000/ - message = "#{push_data[:user_name]} pushed new branch \"#{ref}\"." - elsif after =~ /000000/ - message = "#{push_data[:user_name]} deleted branch \"#{ref}\"." + if Gitlab::Git.blank_ref?(before) + message = "#{data[:user_name]} pushed new branch \"#{ref}\"." + elsif Gitlab::Git.blank_ref?(after) + message = "#{data[:user_name]} deleted branch \"#{ref}\"." else - message = "#{push_data[:user_name]} push to branch \"#{ref}\"." + message = "#{data[:user_name]} push to branch \"#{ref}\"." end - if push_data[:total_commits_count] > 0 - message << "\nTotal commits count: #{push_data[:total_commits_count]}" + if data[:total_commits_count] > 0 + message << "\nTotal commits count: #{data[:total_commits_count]}" end pushover_data = { @@ -99,7 +111,7 @@ class PushoverService < Service priority: priority, title: "#{project.name_with_namespace}", message: message, - url: push_data[:repository][:homepage], + url: data[:repository][:homepage], url_title: "See project #{project.name_with_namespace}" } diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb new file mode 100644 index 00000000000..dd9ba97ee1f --- /dev/null +++ b/app/models/project_services/redmine_service.rb @@ -0,0 +1,44 @@ +# == 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 +# + +class RedmineService < IssueTrackerService + + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + + def title + if self.properties && self.properties['title'].present? + self.properties['title'] + else + 'Redmine' + end + end + + def description + if self.properties && self.properties['description'].present? + self.properties['description'] + else + 'Redmine issue tracker' + end + end + + def to_param + 'redmine' + end +end diff --git a/app/models/project_services/slack_message.rb b/app/models/project_services/slack_message.rb deleted file mode 100644 index 28204e5ea60..00000000000 --- a/app/models/project_services/slack_message.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'slack-notifier' - -class SlackMessage - def initialize(params) - @after = params.fetch(:after) - @before = params.fetch(:before) - @commits = params.fetch(:commits, []) - @project_name = params.fetch(:project_name) - @project_url = params.fetch(:project_url) - @ref = params.fetch(:ref).gsub('refs/heads/', '') - @username = params.fetch(:user_name) - end - - def pretext - format(message) - end - - def attachments - return [] if new_branch? || removed_branch? - - commit_message_attachments - end - - private - - attr_reader :after - attr_reader :before - attr_reader :commits - attr_reader :project_name - attr_reader :project_url - attr_reader :ref - attr_reader :username - - def message - if new_branch? - new_branch_message - elsif removed_branch? - removed_branch_message - else - push_message - end - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end - - def new_branch_message - "#{username} pushed new branch #{branch_link} to #{project_link}" - end - - def removed_branch_message - "#{username} removed branch #{ref} from #{project_link}" - end - - def push_message - "#{username} pushed to branch #{branch_link} of #{project_link} (#{compare_link})" - end - - def commit_messages - commits.each_with_object('') do |commit, str| - str << compose_commit_message(commit) - end.chomp - end - - def commit_message_attachments - [{ text: format(commit_messages), color: attachment_color }] - end - - def compose_commit_message(commit) - author = commit.fetch(:author).fetch(:name) - id = commit.fetch(:id)[0..8] - message = commit.fetch(:message) - url = commit.fetch(:url) - - "[#{id}](#{url}): #{message} - #{author}\n" - end - - def new_branch? - before =~ /000000/ - end - - def removed_branch? - after =~ /000000/ - end - - def branch_url - "#{project_url}/commits/#{ref}" - end - - def compare_url - "#{project_url}/compare/#{before}...#{after}" - end - - def branch_link - "[#{ref}](#{branch_url})" - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def compare_link - "[Compare changes](#{compare_url})" - end - - def attachment_color - '#345' - end -end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 963f5440b6f..36d9874edd3 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -2,18 +2,24 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# 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 # class SlackService < Service - prop_accessor :webhook + prop_accessor :webhook, :username, :channel validates :webhook, presence: true, if: :activated? def title @@ -30,20 +36,52 @@ class SlackService < Service def fields [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' } + { type: 'text', name: 'webhook', + placeholder: 'https://hooks.slack.com/services/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'text', name: 'channel', placeholder: '#channel' } ] end - def execute(push_data) + def supported_events + %w(push issue merge_request note tag_push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) return unless webhook.present? - message = SlackMessage.new(push_data.merge( + object_kind = data[:object_kind] + + data = data.merge( project_url: project_url, project_name: project_name - )) + ) + + # WebHook events often have an 'update' event that follows a 'open' or + # 'close' action. Ignore update events for now to prevent duplicate + # messages from arriving. + + message = \ + case object_kind + when "push", "tag_push" + PushMessage.new(data) + when "issue" + IssueMessage.new(data) unless is_update?(data) + when "merge_request" + MergeMessage.new(data) unless is_update?(data) + when "note" + NoteMessage.new(data) + end - notifier = Slack::Notifier.new(webhook) - notifier.ping(message.pretext, attachments: message.attachments) + opt = {} + opt[:channel] = channel if channel + opt[:username] = username if username + + if message + notifier = Slack::Notifier.new(webhook, opt) + notifier.ping(message.pretext, attachments: message.attachments) + end end private @@ -55,4 +93,13 @@ class SlackService < Service def project_url project.web_url end + + def is_update?(data) + data[:object_attributes][:action] == 'update' + end end + +require "slack_service/issue_message" +require "slack_service/push_message" +require "slack_service/merge_message" +require "slack_service/note_message" diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/slack_service/base_message.rb new file mode 100644 index 00000000000..aa00d6061a1 --- /dev/null +++ b/app/models/project_services/slack_service/base_message.rb @@ -0,0 +1,31 @@ +require 'slack-notifier' + +class SlackService + class BaseMessage + def initialize(params) + raise NotImplementedError + end + + def pretext + format(message) + end + + def attachments + raise NotImplementedError + end + + private + + def message + raise NotImplementedError + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def attachment_color + '#345' + end + end +end diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb new file mode 100644 index 00000000000..5af24a80609 --- /dev/null +++ b/app/models/project_services/slack_service/issue_message.rb @@ -0,0 +1,56 @@ +class SlackService + class IssueMessage < BaseMessage + attr_reader :user_name + attr_reader :title + attr_reader :project_name + attr_reader :project_url + attr_reader :issue_iid + attr_reader :issue_url + attr_reader :action + attr_reader :state + attr_reader :description + + def initialize(params) + @user_name = params[:user][:name] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @title = obj_attr[:title] + @issue_iid = obj_attr[:iid] + @issue_url = obj_attr[:url] + @action = obj_attr[:action] + @state = obj_attr[:state] + @description = obj_attr[:description] + end + + def attachments + return [] unless opened_issue? + + description_message + end + + private + + def message + "#{user_name} #{state} #{issue_link} in #{project_link}: *#{title}*" + end + + def opened_issue? + action == "open" + end + + def description_message + [{ text: format(description), color: attachment_color }] + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def issue_link + "[issue ##{issue_iid}](#{issue_url})" + end + end +end diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb new file mode 100644 index 00000000000..e792c258f73 --- /dev/null +++ b/app/models/project_services/slack_service/merge_message.rb @@ -0,0 +1,60 @@ +class SlackService + class MergeMessage < BaseMessage + attr_reader :user_name + attr_reader :project_name + attr_reader :project_url + attr_reader :merge_request_id + attr_reader :source_branch + attr_reader :target_branch + attr_reader :state + attr_reader :title + + def initialize(params) + @user_name = params[:user][:name] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @merge_request_id = obj_attr[:iid] + @source_branch = obj_attr[:source_branch] + @target_branch = obj_attr[:target_branch] + @state = obj_attr[:state] + @title = format_title(obj_attr[:title]) + end + + def pretext + format(message) + end + + def attachments + [] + end + + private + + def format_title(title) + '*' + title.lines.first.chomp + '*' + end + + def message + merge_request_message + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def merge_request_message + "#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}" + end + + def merge_request_link + "[merge request ##{merge_request_id}](#{merge_request_url})" + end + + def merge_request_url + "#{project_url}/merge_requests/#{merge_request_id}" + end + end +end diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb new file mode 100644 index 00000000000..074478b292d --- /dev/null +++ b/app/models/project_services/slack_service/note_message.rb @@ -0,0 +1,82 @@ +class SlackService + class NoteMessage < BaseMessage + attr_reader :message + attr_reader :user_name + attr_reader :project_name + attr_reader :project_link + attr_reader :note + attr_reader :note_url + attr_reader :title + + def initialize(params) + params = HashWithIndifferentAccess.new(params) + @user_name = params[:user][:name] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @note = obj_attr[:note] + @note_url = obj_attr[:url] + noteable_type = obj_attr[:noteable_type] + + case noteable_type + when "Commit" + create_commit_note(HashWithIndifferentAccess.new(params[:commit])) + when "Issue" + create_issue_note(HashWithIndifferentAccess.new(params[:issue])) + when "MergeRequest" + create_merge_note(HashWithIndifferentAccess.new(params[:merge_request])) + when "Snippet" + create_snippet_note(HashWithIndifferentAccess.new(params[:snippet])) + end + end + + def attachments + description_message + end + + private + + def format_title(title) + title.lines.first.chomp + end + + def create_commit_note(commit) + commit_sha = commit[:id] + commit_sha = Commit.truncate_sha(commit_sha) + commit_link = "[commit #{commit_sha}](#{@note_url})" + title = format_title(commit[:message]) + @message = "#{@user_name} commented on #{commit_link} in #{project_link}: *#{title}*" + end + + def create_issue_note(issue) + issue_iid = issue[:iid] + note_link = "[issue ##{issue_iid}](#{@note_url})" + title = format_title(issue[:title]) + @message = "#{@user_name} commented on #{note_link} in #{project_link}: *#{title}*" + end + + def create_merge_note(merge_request) + merge_request_id = merge_request[:iid] + merge_request_link = "[merge request ##{merge_request_id}](#{@note_url})" + title = format_title(merge_request[:title]) + @message = "#{@user_name} commented on #{merge_request_link} in #{project_link}: *#{title}*" + end + + def create_snippet_note(snippet) + snippet_id = snippet[:id] + snippet_link = "[snippet ##{snippet_id}](#{@note_url})" + title = format_title(snippet[:title]) + @message = "#{@user_name} commented on #{snippet_link} in #{project_link}: *#{title}*" + end + + def description_message + [{ text: format(@note), color: attachment_color }] + end + + def project_link + "[#{@project_name}](#{@project_url})" + end + end +end diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/slack_service/push_message.rb new file mode 100644 index 00000000000..b26f3e9ddce --- /dev/null +++ b/app/models/project_services/slack_service/push_message.rb @@ -0,0 +1,110 @@ +class SlackService + class PushMessage < BaseMessage + attr_reader :after + attr_reader :before + attr_reader :commits + attr_reader :project_name + attr_reader :project_url + attr_reader :ref + attr_reader :ref_type + attr_reader :user_name + + def initialize(params) + @after = params[:after] + @before = params[:before] + @commits = params.fetch(:commits, []) + @project_name = params[:project_name] + @project_url = params[:project_url] + @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch' + @ref = Gitlab::Git.ref_name(params[:ref]) + @user_name = params[:user_name] + end + + def pretext + format(message) + end + + def attachments + return [] if new_branch? || removed_branch? + + commit_message_attachments + end + + private + + def message + if new_branch? + new_branch_message + elsif removed_branch? + removed_branch_message + else + push_message + end + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def new_branch_message + "#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}" + end + + def removed_branch_message + "#{user_name} removed #{ref_type} #{ref} from #{project_link}" + end + + def push_message + "#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})" + end + + def commit_messages + commits.map { |commit| compose_commit_message(commit) }.join("\n") + end + + def commit_message_attachments + [{ text: format(commit_messages), color: attachment_color }] + end + + def compose_commit_message(commit) + author = commit[:author][:name] + id = Commit.truncate_sha(commit[:id]) + message = commit[:message] + url = commit[:url] + + "[#{id}](#{url}): #{message} - #{author}" + end + + def new_branch? + Gitlab::Git.blank_ref?(before) + end + + def removed_branch? + Gitlab::Git.blank_ref?(after) + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def compare_url + "#{project_url}/compare/#{before}...#{after}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def compare_link + "[Compare changes](#{compare_url})" + end + + def attachment_color + '#345' + end + end +end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb new file mode 100644 index 00000000000..3c002a1634b --- /dev/null +++ b/app/models/project_services/teamcity_service.rb @@ -0,0 +1,145 @@ +# == 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 +# + +class TeamcityService < CiService + include HTTParty + + prop_accessor :teamcity_url, :build_type, :username, :password + + validates :teamcity_url, + presence: true, + format: { with: /\A#{URI.regexp}\z/ }, if: :activated? + validates :build_type, presence: true, if: :activated? + validates :username, + presence: true, + if: ->(service) { service.password? }, if: :activated? + validates :password, + presence: true, + if: ->(service) { service.username? }, if: :activated? + + attr_accessor :response + + after_save :compose_service_hook, if: :activated? + + def compose_service_hook + hook = service_hook || build_service_hook + hook.save + end + + def title + 'JetBrains TeamCity CI' + end + + def description + 'A continuous integration and build server' + end + + def help + 'The build configuration in Teamcity must use the build format '\ + 'number %build.vcs.number% '\ + 'you will also want to configure monitoring of all branches so merge '\ + 'requests build, that setting is in the vsc root advanced settings.' + end + + def to_param + 'teamcity' + end + + def supported_events + %w(push) + end + + def fields + [ + { type: 'text', name: 'teamcity_url', + placeholder: 'TeamCity root URL like https://teamcity.example.com' }, + { type: 'text', name: 'build_type', + placeholder: 'Build configuration ID' }, + { type: 'text', name: 'username', + placeholder: 'A user with permissions to trigger a manual build' }, + { type: 'password', name: 'password' }, + ] + end + + def build_info(sha) + url = URI.parse("#{teamcity_url}/httpAuth/app/rest/builds/"\ + "branch:unspecified:any,number:#{sha}") + auth = { + username: username, + password: password, + } + @response = HTTParty.get("#{url}", verify: false, basic_auth: auth) + end + + def build_page(sha, ref) + build_info(sha) if @response.nil? || !@response.code + + if @response.code != 200 + # If actual build link can't be determined, + # send user to build summary page. + "#{teamcity_url}/viewLog.html?buildTypeId=#{build_type}" + else + # If actual build link is available, go to build result page. + built_id = @response['build']['id'] + "#{teamcity_url}/viewLog.html?buildId=#{built_id}"\ + "&buildTypeId=#{build_type}" + end + end + + def commit_status(sha, ref) + build_info(sha) if @response.nil? || !@response.code + return :error unless @response.code == 200 || @response.code == 404 + + status = if @response.code == 404 + 'Pending' + else + @response['build']['status'] + end + + if status.include?('SUCCESS') + 'success' + elsif status.include?('FAILURE') + 'failed' + elsif status.include?('Pending') + 'pending' + else + :error + end + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + auth = { + username: username, + password: password, + } + + branch = Gitlab::Git.ref_name(data[:ref]) + + self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue", + body: "<build branchName=\"#{branch}\">"\ + "<buildType id=\"#{build_type}\"/>"\ + '</build>', + headers: { 'Content-type' => 'application/xml' }, + basic_auth: auth + ) + end +end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 657ee23ae23..56e49af2324 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -12,12 +12,12 @@ class ProjectTeam # @team << [@users, :master] # def <<(args) - users = args.first + users, access, current_user = *args if users.respond_to?(:each) - add_users(users, args.second) + add_users(users, access, current_user) else - add_user(users, args.second) + add_user(users, access, current_user) end end @@ -31,34 +31,31 @@ class ProjectTeam user end - def find_tm(user_id) - tm = project.project_members.find_by(user_id: user_id) + def find_member(user_id) + member = project.project_members.find_by(user_id: user_id) # If user is not in project members # we should check for group membership - if group && !tm - tm = group.group_members.find_by(user_id: user_id) + if group && !member + member = group.group_members.find_by(user_id: user_id) end - tm + member end - def add_user(user, access) - add_users_ids([user.id], access) - end - - def add_users(users, access) - add_users_ids(users.map(&:id), access) - end - - def add_users_ids(user_ids, access) + def add_users(users, access, current_user = nil) ProjectMember.add_users_into_projects( [project.id], - user_ids, - access + users, + access, + current_user ) end + def add_user(user, access, current_user = nil) + add_users([user], access, current_user) + end + # Remove all users from project team def truncate ProjectMember.truncate_team(project) @@ -88,27 +85,28 @@ class ProjectTeam @masters ||= fetch_members(:masters) end - def import(source_project) + def import(source_project, current_user = nil) target_project = project - source_team = source_project.project_members.to_a + source_members = source_project.project_members.to_a target_user_ids = target_project.project_members.pluck(:user_id) - source_team.reject! do |tm| + source_members.reject! do |member| # Skip if user already present in team - target_user_ids.include?(tm.user_id) + !member.invite? && target_user_ids.include?(member.user_id) end - source_team.map! do |tm| - new_tm = tm.dup - new_tm.id = nil - new_tm.source = target_project - new_tm + source_members.map! do |member| + new_member = member.dup + new_member.id = nil + new_member.source = target_project + new_member.created_by = current_user + new_member end ProjectMember.transaction do - source_team.each do |tm| - tm.save + source_members.each do |member| + member.save end end @@ -118,26 +116,26 @@ class ProjectTeam end def guest?(user) - max_tm_access(user.id) == Gitlab::Access::GUEST + max_member_access(user.id) == Gitlab::Access::GUEST end def reporter?(user) - max_tm_access(user.id) == Gitlab::Access::REPORTER + max_member_access(user.id) == Gitlab::Access::REPORTER end def developer?(user) - max_tm_access(user.id) == Gitlab::Access::DEVELOPER + max_member_access(user.id) == Gitlab::Access::DEVELOPER end def master?(user) - max_tm_access(user.id) == Gitlab::Access::MASTER + max_member_access(user.id) == Gitlab::Access::MASTER end def member?(user_id) - !!find_tm(user_id) + !!find_member(user_id) end - def max_tm_access(user_id) + def max_member_access(user_id) access = [] access << project.project_members.find_by(user_id: user_id).try(:access_field) @@ -160,7 +158,7 @@ class ProjectTeam end user_ids = project_members.pluck(:user_id) - user_ids += group_members.pluck(:user_id) if group + user_ids.push(*group_members.pluck(:user_id)) if group User.where(id: user_ids) end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 770a26ed894..231973fa543 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -2,10 +2,10 @@ class ProjectWiki include Gitlab::ShellAdapter MARKUPS = { - 'Markdown' => :markdown, + 'Markdown' => :md, 'RDoc' => :rdoc, 'AsciiDoc' => :asciidoc - } + } unless defined?(MARKUPS) class CouldNotCreateWikiError < StandardError; end @@ -104,7 +104,7 @@ class ProjectWiki def page_title_and_dir(title) title_array = title.split("/") title = title_array.pop - [title.gsub(/\.[^.]*$/, ""), title_array.join("/")] + [title, title_array.join("/")] end def search_files(query) @@ -112,7 +112,7 @@ class ProjectWiki end def repository - Repository.new(path_with_namespace, default_branch) + Repository.new(path_with_namespace, default_branch, @project) end def default_branch @@ -136,7 +136,7 @@ class ProjectWiki def commit_details(action, message = nil, title = nil) commit_message = message || default_message(action, title) - {email: @user.email, name: @user.name, message: commit_message} + { email: @user.email, name: @user.name, message: commit_message } end def default_message(action, title) diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 1b06dd77523..8ebd790a89e 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -2,11 +2,12 @@ # # 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 +# 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 # class ProtectedBranch < ActiveRecord::Base @@ -17,6 +18,6 @@ class ProtectedBranch < ActiveRecord::Base validates :project, presence: true def commit - project.repository.commit(self.name) + project.commit(self.name) end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 93994123a90..b32e8847bb5 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1,11 +1,17 @@ class Repository include Gitlab::ShellAdapter - attr_accessor :raw_repository, :path_with_namespace + attr_accessor :raw_repository, :path_with_namespace, :project - def initialize(path_with_namespace, default_branch = nil) + def initialize(path_with_namespace, default_branch = nil, project = nil) @path_with_namespace = path_with_namespace - @raw_repository = Gitlab::Git::Repository.new(path_to_repo) if path_with_namespace + @project = project + + if path_with_namespace + @raw_repository = Gitlab::Git::Repository.new(path_to_repo) + @raw_repository.autocrlf = :input + end + rescue Gitlab::Git::Repository::NoRepository nil end @@ -28,9 +34,9 @@ class Repository def commit(id = 'HEAD') return nil unless raw_repository commit = Gitlab::Git::Commit.find(raw_repository, id) - commit = Commit.new(commit) if commit + commit = Commit.new(commit, @project) if commit commit - rescue Rugged::OdbError => ex + rescue Rugged::OdbError nil end @@ -42,13 +48,13 @@ class Repository limit: limit, offset: offset, ) - commits = Commit.decorate(commits) if commits.present? + commits = Commit.decorate(commits, @project) if commits.present? commits end def commits_between(from, to) commits = Gitlab::Git::Commit.between(raw_repository, from, to) - commits = Commit.decorate(commits) if commits.present? + commits = Commit.decorate(commits, @project) if commits.present? commits end @@ -61,25 +67,29 @@ class Repository end def add_branch(branch_name, ref) - Rails.cache.delete(cache_key(:branch_names)) + cache.expire(:branch_names) + @branches = nil gitlab_shell.add_branch(path_with_namespace, branch_name, ref) end def add_tag(tag_name, ref, message = nil) - Rails.cache.delete(cache_key(:tag_names)) + cache.expire(:tag_names) + @tags = nil gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) end def rm_branch(branch_name) - Rails.cache.delete(cache_key(:branch_names)) + cache.expire(:branch_names) + @branches = nil gitlab_shell.rm_branch(path_with_namespace, branch_name) end def rm_tag(tag_name) - Rails.cache.delete(cache_key(:tag_names)) + cache.expire(:tag_names) + @tags = nil gitlab_shell.rm_tag(path_with_namespace, tag_name) end @@ -97,19 +107,15 @@ class Repository end def branch_names - Rails.cache.fetch(cache_key(:branch_names)) do - raw_repository.branch_names - end + cache.fetch(:branch_names) { raw_repository.branch_names } end def tag_names - Rails.cache.fetch(cache_key(:tag_names)) do - raw_repository.tag_names - end + cache.fetch(:tag_names) { raw_repository.tag_names } end def commit_count - Rails.cache.fetch(cache_key(:commit_count)) do + cache.fetch(:commit_count) do begin raw_repository.commit_count(self.root_ref) rescue @@ -121,51 +127,49 @@ class Repository # Return repo size in megabytes # Cached in redis def size - Rails.cache.fetch(cache_key(:size)) do - raw_repository.size - end + cache.fetch(:size) { raw_repository.size } end def expire_cache - Rails.cache.delete(cache_key(:size)) - Rails.cache.delete(cache_key(:branch_names)) - Rails.cache.delete(cache_key(:tag_names)) - Rails.cache.delete(cache_key(:commit_count)) - Rails.cache.delete(cache_key(:graph_log)) - Rails.cache.delete(cache_key(:readme)) - Rails.cache.delete(cache_key(:version)) - Rails.cache.delete(cache_key(:contribution_guide)) + %i(size branch_names tag_names commit_count graph_log + readme version contribution_guide changelog license).each do |key| + cache.expire(key) + end end def graph_log - Rails.cache.fetch(cache_key(:graph_log)) do + cache.fetch(:graph_log) do commits = raw_repository.log(limit: 6000, skip_merges: true, ref: root_ref) + commits.map do |rugged_commit| commit = Gitlab::Git::Commit.new(rugged_commit) { - author_name: commit.author_name.force_encoding('UTF-8'), - author_email: commit.author_email.force_encoding('UTF-8'), + author_name: commit.author_name, + author_email: commit.author_email, additions: commit.stats.additions, - deletions: commit.stats.deletions + deletions: commit.stats.deletions, } end end end - def cache_key(type) - "#{type}:#{path_with_namespace}" + def lookup_cache + @lookup_cache ||= {} end def method_missing(m, *args, &block) - raw_repository.send(m, *args, &block) + if m == :lookup && !block_given? + lookup_cache[m] ||= {} + lookup_cache[m][args.join(":")] ||= raw_repository.send(m, *args, &block) + else + raw_repository.send(m, *args, &block) + end end - def respond_to?(method) - return true if raw_repository.respond_to?(method) - - super + def respond_to_missing?(method, include_private = false) + raw_repository.respond_to?(method, include_private) || super end def blob_at(sha, path) @@ -177,13 +181,11 @@ class Repository end def readme - Rails.cache.fetch(cache_key(:readme)) do - tree(:head).readme - end + cache.fetch(:readme) { tree(:head).readme } end def version - Rails.cache.fetch(cache_key(:version)) do + cache.fetch(:version) do tree(:head).blobs.find do |file| file.name.downcase == 'version' end @@ -191,18 +193,44 @@ class Repository end def contribution_guide - Rails.cache.fetch(cache_key(:contribution_guide)) do - tree(:head).contribution_guide + cache.fetch(:contribution_guide) do + tree(:head).blobs.find do |file| + file.contributing? + end + end + end + + def changelog + cache.fetch(:changelog) do + tree(:head).blobs.find do |file| + file.name =~ /\A(changelog|history)/i + end + end + end + + def license + cache.fetch(:license) do + tree(:head).blobs.find do |file| + file.name =~ /\Alicense/i + end end end def head_commit - commit(self.root_ref) + @head_commit ||= commit(self.root_ref) + end + + def head_tree + @head_tree ||= Tree.new(self, head_commit.sha, nil) end def tree(sha = :head, path = nil) if sha == :head - sha = head_commit.sha + if path.nil? + return head_tree + else + sha = head_commit.sha + end end Tree.new(self, sha, path) @@ -235,7 +263,7 @@ class Repository end def last_commit_for_path(sha, path) - args = %W(git rev-list --max-count 1 #{sha} -- #{path}) + args = %W(git rev-list --max-count=1 #{sha} -- #{path}) sha = Gitlab::Popen.popen(args, path_to_repo).first.strip commit(sha) end @@ -243,6 +271,9 @@ class Repository # Remove archives older than 2 hours def clean_old_archives repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path + + return unless File.directory?(repository_downloads_path) + Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) end @@ -312,4 +343,86 @@ class Repository [] end end + + def tag_names_contains(sha) + args = %W(git tag --contains #{sha}) + names = Gitlab::Popen.popen(args, path_to_repo).first + + if names.respond_to?(:split) + names = names.split("\n").map(&:strip) + + names.each do |name| + name.slice! '* ' + end + + names + else + [] + end + end + + def branches + @branches ||= raw_repository.branches + end + + def tags + @tags ||= raw_repository.tags + end + + def root_ref + @root_ref ||= raw_repository.root_ref + end + + def commit_file(user, path, content, message, ref) + path[0] = '' if path[0] == '/' + + committer = user_to_comitter(user) + options = {} + options[:committer] = committer + options[:author] = committer + options[:commit] = { + message: message, + branch: ref + } + + options[:file] = { + content: content, + path: path + } + + Gitlab::Git::Blob.commit(raw_repository, options) + end + + def remove_file(user, path, message, ref) + path[0] = '' if path[0] == '/' + + committer = user_to_comitter(user) + options = {} + options[:committer] = committer + options[:author] = committer + options[:commit] = { + message: message, + branch: ref + } + + options[:file] = { + path: path + } + + Gitlab::Git::Blob.remove(raw_repository, options) + end + + private + + def user_to_comitter(user) + { + email: user.email, + name: user.name, + time: Time.now + } + end + + def cache + @cache ||= RepositoryCache.new(path_with_namespace) + end end diff --git a/app/models/service.rb b/app/models/service.rb index c489c1e96e1..818a6808db5 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -2,34 +2,58 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# 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 # # To add new service you should build a class inherited from Service # and implement a set of methods class Service < ActiveRecord::Base + include Sortable serialize :properties, JSON default_value_for :active, false + default_value_for :push_events, true + default_value_for :issues_events, true + default_value_for :merge_requests_events, true + default_value_for :tag_push_events, true + default_value_for :note_events, true after_initialize :initialize_properties belongs_to :project has_one :service_hook - validates :project_id, presence: true + validates :project_id, presence: true, unless: Proc.new { |service| service.template? } + + scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } + + scope :push_hooks, -> { where(push_events: true, active: true) } + scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) } + scope :issue_hooks, -> { where(issues_events: true, active: true) } + scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } + scope :note_hooks, -> { where(note_events: true, active: true) } def activated? active end + def template? + template + end + def category :common end @@ -59,6 +83,10 @@ class Service < ActiveRecord::Base [] end + def supported_events + %w(push tag_push issue merge_request) + end + def execute # implement inside child end @@ -82,4 +110,45 @@ class Service < ActiveRecord::Base } end end + + def async_execute(data) + return unless supported_events.include?(data[:object_kind]) + + Sidekiq::Client.enqueue(ProjectServiceWorker, id, data) + end + + def issue_tracker? + self.category == :issue_tracker + end + + def self.available_services_names + %w( + asana + assembla + bamboo + buildkite + campfire + custom_issue_tracker + emails_on_push + external_wiki + flowdock + gemnasium + gitlab_ci + hipchat + irker + jira + pivotaltracker + pushover + redmine + slack + teamcity + ) + end + + def self.create_from_template(project_id, template) + service = template.dup + service.template = false + service.project_id = project_id + service if service.save + end end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index a47fbca3260..b0831982aa7 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -16,12 +16,16 @@ # class Snippet < ActiveRecord::Base - include Linguist::BlobHelper include Gitlab::VisibilityLevel + include Linguist::BlobHelper + include Participable + include Referable + include Sortable default_value_for :visibility_level, Snippet::PRIVATE - belongs_to :author, class_name: "User" + belongs_to :author, class_name: 'User' + belongs_to :project has_many :notes, as: :noteable, dependent: :destroy @@ -29,7 +33,10 @@ class Snippet < ActiveRecord::Base validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } - validates :file_name, presence: true, length: { within: 0..255 } + validates :file_name, + length: { within: 0..255 }, + format: { with: Gitlab::Regex.file_name_regex, + message: Gitlab::Regex.file_name_regex_message } validates :content, presence: true validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values } @@ -42,6 +49,32 @@ class Snippet < ActiveRecord::Base scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } + participant :author, :notes + + def self.reference_prefix + '$' + end + + # Pattern used to extract `$123` snippet references from text + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?<snippet>\d+) + }x + end + + def to_reference(from_project = nil) + reference = "#{self.class.reference_prefix}#{id}" + + if cross_project_reference?(from_project) + reference = project.to_reference + reference + end + + reference + end + def self.content_types [ ".rb", ".py", ".pl", ".scala", ".c", ".cpp", ".java", @@ -54,6 +87,10 @@ class Snippet < ActiveRecord::Base content end + def hook_attrs + attributes + end + def size 0 end @@ -62,6 +99,10 @@ class Snippet < ActiveRecord::Base file_name end + def sanitized_file_name + file_name.gsub(/[^a-zA-Z0-9_\-\.]+/, '') + end + def mode nil end @@ -72,7 +113,7 @@ class Snippet < ActiveRecord::Base def visibility_level_field visibility_level - end + end class << self def search(query) diff --git a/app/models/subscription.rb b/app/models/subscription.rb new file mode 100644 index 00000000000..dd75d3ab8ba --- /dev/null +++ b/app/models/subscription.rb @@ -0,0 +1,21 @@ +# == Schema Information +# +# Table name: subscriptions +# +# id :integer not null, primary key +# user_id :integer +# subscribable_id :integer +# subscribable_type :string(255) +# subscribed :boolean +# created_at :datetime +# updated_at :datetime +# + +class Subscription < ActiveRecord::Base + belongs_to :user + belongs_to :subscribable, polymorphic: true + + validates :user_id, + uniqueness: { scope: [:subscribable_id, :subscribable_type] }, + presence: true +end diff --git a/app/models/tree.rb b/app/models/tree.rb index 4f5d81f0a5e..93b3246a668 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -1,38 +1,38 @@ class Tree - include Gitlab::MarkdownHelper + include Gitlab::MarkupHelper - attr_accessor :entries, :readme, :contribution_guide + attr_accessor :repository, :sha, :path, :entries def initialize(repository, sha, path = '/') path = '/' if path.blank? - git_repo = repository.raw_repository - @entries = Gitlab::Git::Tree.where(git_repo, sha, path) - - available_readmes = @entries.select(&:readme?) - - if available_readmes.count > 0 - # If there is more than 1 readme in tree, find readme which is supported - # by markup renderer. - if available_readmes.length > 1 - supported_readmes = available_readmes.select do |readme| - previewable?(readme.name) - end - - # Take the first supported readme, or the first available readme, if we - # don't support any of them - readme_tree = supported_readmes.first || available_readmes.first - else - readme_tree = available_readmes.first - end - - readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name) - @readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path) - end - if contribution_tree = @entries.find(&:contributing?) - contribution_path = path == '/' ? contribution_tree.name : File.join(path, contribution_tree.name) - @contribution_guide = Gitlab::Git::Blob.find(git_repo, sha, contribution_path) + @repository = repository + @sha = sha + @path = path + + git_repo = @repository.raw_repository + @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path) + end + + def readme + return @readme if defined?(@readme) + + available_readmes = blobs.select(&:readme?) + + if available_readmes.count == 0 + return @readme = nil end + + # Take the first previewable readme, or the first available readme, if we + # can't preview any of them + readme_tree = available_readmes.find do |readme| + previewable?(readme.name) + end || available_readmes.first + + readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name) + + git_repo = repository.raw_repository + @readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path) end def trees diff --git a/app/models/user.rb b/app/models/user.rb index 1cddd85ada0..29f43051464 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,67 +2,90 @@ # # 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 -# extern_uid :string(255) -# provider :string(255) -# 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 +# 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 +# github_access_token :string(255) +# gitlab_access_token :string(255) +# notification_email :string(255) +# hide_no_password :boolean default(FALSE) +# password_automatically_set :boolean default(FALSE) +# bitbucket_access_token :string(255) +# bitbucket_access_token_secret :string(255) +# location :string(255) +# public_email :string(255) default(""), not null +# encrypted_otp_secret :string(255) +# encrypted_otp_secret_iv :string(255) +# encrypted_otp_secret_salt :string(255) +# otp_required_for_login :boolean +# otp_backup_codes :text +# dashboard :integer default(0) # require 'carrierwave/orm/activerecord' require 'file_size_validator' class User < ActiveRecord::Base - include Gitlab::ConfigHelper extend Gitlab::ConfigHelper + + include Gitlab::ConfigHelper + include Gitlab::CurrentSettings + include Referable + include Sortable include TokenAuthenticatable default_value_for :admin, false default_value_for :can_create_group, gitlab_config.default_can_create_group default_value_for :can_create_team, false default_value_for :hide_no_ssh_key, false - default_value_for :projects_limit, gitlab_config.default_projects_limit + default_value_for :hide_no_password, false default_value_for :theme_id, gitlab_config.default_theme - devise :database_authenticatable, :lockable, :async, - :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable + devise :two_factor_authenticatable, + otp_secret_encryption_key: File.read(Rails.root.join('.secret')).chomp + + devise :two_factor_backupable, otp_number_of_backup_codes: 10 + serialize :otp_backup_codes, JSON + + devise :lockable, :async, :recoverable, :rememberable, :trackable, + :validatable, :omniauthable, :confirmable, :registerable attr_accessor :force_random_password @@ -79,6 +102,7 @@ class User < ActiveRecord::Base # Profile has_many :keys, dependent: :destroy has_many :emails, dependent: :destroy + has_many :identities, dependent: :destroy # Groups has_many :members, dependent: :destroy @@ -102,62 +126,61 @@ class User < ActiveRecord::Base has_many :notes, dependent: :destroy, foreign_key: :author_id has_many :merge_requests, dependent: :destroy, foreign_key: :author_id has_many :events, dependent: :destroy, foreign_key: :author_id, class_name: "Event" + has_many :subscriptions, dependent: :destroy has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event" has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue" has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" + has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # # Validations # validates :name, presence: true - validates :email, presence: true, email: {strict_mode: true}, uniqueness: true + # Note that a 'uniqueness' and presence check is provided by devise :validatable for email. We do not need to + # duplicate that here as the validation framework will have duplicate errors in the event of a failure. + validates :email, presence: true, email: { strict_mode: true } + validates :notification_email, presence: true, email: { strict_mode: true } + validates :public_email, presence: true, email: { strict_mode: true }, allow_blank: true, uniqueness: true validates :bio, length: { maximum: 255 }, allow_blank: true - validates :extern_uid, allow_blank: true, uniqueness: {scope: :provider} - validates :projects_limit, presence: true, numericality: {greater_than_or_equal_to: 0} - validates :username, presence: true, uniqueness: { case_sensitive: false }, - exclusion: { in: Gitlab::Blacklist.path }, - format: { with: Gitlab::Regex.username_regex, - message: Gitlab::Regex.username_regex_message } + validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } + validates :username, + presence: true, + uniqueness: { case_sensitive: false }, + exclusion: { in: Gitlab::Blacklist.path }, + format: { with: Gitlab::Regex.namespace_regex, + message: Gitlab::Regex.namespace_regex_message } validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } - validate :avatar_type, if: ->(user) { user.avatar_changed? } + validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :unique_email, if: ->(user) { user.email_changed? } + validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } + validate :owns_public_email, if: ->(user) { user.public_email_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } before_validation :generate_password, on: :create + before_validation :restricted_signup_domains, on: :create before_validation :sanitize_attrs + before_validation :set_notification_email, if: ->(user) { user.email_changed? } + before_validation :set_public_email, if: ->(user) { user.public_email_changed? } + after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? } before_save :ensure_authentication_token after_save :ensure_namespace_correct + after_initialize :set_projects_limit after_create :post_create_hook after_destroy :post_destroy_hook + # User's Dashboard preference + # Note: When adding an option, it MUST go on the end of the array. + enum dashboard: [:projects, :stars] alias_attribute :private_token, :authentication_token delegate :path, to: :namespace, allow_nil: true, prefix: true state_machine :state, initial: :active do - after_transition any => :blocked do |user, transition| - # Remove user from all projects and - user.project_members.find_each do |membership| - # skip owned resources - next if membership.project.owner == user - - return false unless membership.destroy - end - - # Remove user from all groups - user.group_members.find_each do |membership| - # skip owned resources - next if membership.group.last_owner?(user) - - return false unless membership.destroy - end - end - event :block do transition active: :blocked end @@ -167,19 +190,14 @@ class User < ActiveRecord::Base end end - mount_uploader :avatar, AttachmentUploader + mount_uploader :avatar, AvatarUploader # Scopes scope :admins, -> { where(admin: true) } scope :blocked, -> { with_state(:blocked) } scope :active, -> { with_state(:active) } - scope :alphabetically, -> { order('name ASC') } - scope :in_team, ->(team){ where(id: team.member_ids) } - scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) } 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 :ldap, -> { where('provider LIKE ?', 'ldap%') } - scope :potential_team_members, ->(team) { team.members.any? ? active.not_in_team(team) : active } # # Class methods @@ -197,19 +215,34 @@ class User < ActiveRecord::Base def sort(method) case method.to_s - when 'recent_sign_in' then reorder('users.last_sign_in_at DESC') - when 'oldest_sign_in' then reorder('users.last_sign_in_at ASC') - when 'recently_created' then reorder('users.created_at DESC') - when 'late_created' then reorder('users.created_at ASC') - else reorder("users.name ASC") + when 'recent_sign_in' then reorder(last_sign_in_at: :desc) + when 'oldest_sign_in' then reorder(last_sign_in_at: :asc) + else + order_by(method) end end def find_for_commit(email, name) - # Prefer email match over name match - User.where(email: email).first || - User.joins(:emails).where(emails: { email: email }).first || - User.where(name: name).first + user_table = arel_table + email_table = Email.arel_table + + # Use ARel to build a query: + query = user_table. + # SELECT "users".* FROM "users" + project(user_table[Arel.star]). + # LEFT OUTER JOIN "emails" + join(email_table, Arel::Nodes::OuterJoin). + # ON "users"."id" = "emails"."user_id" + on(user_table[:id].eq(email_table[:user_id])). + # WHERE ("user"."email" = '<email>' OR "user"."name" = '<name>') + # OR "emails"."email" = '<email>' + where( + user_table[:email].eq(email). + or(user_table[:name].eq(name)). + or(email_table[:email].eq(email)) + ) + + find_by_sql(query.to_sql).first end def filter(filter_name) @@ -238,6 +271,18 @@ class User < ActiveRecord::Base def build_user(attrs = {}) User.new(attrs) end + + def reference_prefix + '@' + end + + # Pattern used to extract `@user` user references from text + def reference_pattern + %r{ + #{Regexp.escape(reference_prefix)} + (?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR}) + }x + end end # @@ -248,6 +293,10 @@ class User < ActiveRecord::Base username end + def to_reference(_from_project = nil) + "#{self.class.reference_prefix}#{username}" + end + def notification @notification ||= Notification.new(self) end @@ -267,9 +316,22 @@ class User < ActiveRecord::Base @reset_token end + # Check if the user has enabled Two-factor Authentication + def two_factor_enabled? + otp_required_for_login + end + + # Set whether or not Two-factor Authentication is enabled for the current user + # + # setting - Boolean + def two_factor_enabled=(setting) + self.otp_required_for_login = setting + end + def namespace_uniq namespace_name = self.username - if Namespace.find_by(path: namespace_name) + existing_namespace = Namespace.by_path(namespace_name) + if existing_namespace && existing_namespace != self.namespace self.errors.add :username, "already exists" end end @@ -281,14 +343,34 @@ class User < ActiveRecord::Base end def unique_email - self.errors.add(:email, 'has already been taken') if Email.exists?(email: self.email) + if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email) + self.errors.add(:email, 'has already been taken') + end + end + + def owns_notification_email + self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email) + end + + def owns_public_email + self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email) + end + + def update_emails_with_primary_email + primary_email_record = self.emails.find_by(email: self.email) + if primary_email_record + primary_email_record.destroy + self.emails.create(email: self.email_was) + + self.update_secondary_emails! + end end # Groups user has access to def authorized_groups @authorized_groups ||= begin group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id)) - Group.where(id: group_ids).order('namespaces.name ASC') + Group.where(id: group_ids) end end @@ -297,16 +379,18 @@ class User < ActiveRecord::Base def authorized_projects @authorized_projects ||= begin project_ids = personal_projects.pluck(:id) - project_ids += groups_projects.pluck(:id) - project_ids += projects.pluck(:id).uniq - Project.where(id: project_ids).joins(:namespace).order('namespaces.name ASC') + project_ids.push(*groups_projects.pluck(:id)) + project_ids.push(*projects.pluck(:id).uniq) + Project.where(id: project_ids) end end def owned_projects - @owned_projects ||= begin - Project.where(namespace_id: owned_groups.pluck(:id).push(namespace.id)).joins(:namespace) - end + @owned_projects ||= + begin + namespace_ids = owned_groups.pluck(:id).push(namespace.id) + Project.in_namespace(namespace_ids).joins(:namespace) + end end # Team membership in authorized projects @@ -322,6 +406,10 @@ class User < ActiveRecord::Base keys.count == 0 end + def require_password? + password_automatically_set? && !ldap_user? + end + def can_change_username? gitlab_config.username_changing_enabled end @@ -389,7 +477,7 @@ class User < ActiveRecord::Base end def tm_of(project) - project.team_member_by_id(self.id) + project.project_member_by_id(self.id) end def already_forked?(project) @@ -407,11 +495,23 @@ class User < ActiveRecord::Base end def ldap_user? - extern_uid && provider.start_with?('ldap') + identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"]) + end + + def ldap_identity + @ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"]) + end + + def project_deploy_keys + DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id) end def accessible_deploy_keys - DeployKey.in_projects(self.authorized_projects.pluck(:id)).uniq + @accessible_deploy_keys ||= begin + key_ids = project_deploy_keys.pluck(:id) + key_ids.push(*DeployKey.are_public.pluck(:id)) + DeployKey.where(id: key_ids) + end end def created_by @@ -419,12 +519,37 @@ class User < ActiveRecord::Base end def sanitize_attrs - %w(name username skype linkedin twitter bio).each do |attr| + %w(name username skype linkedin twitter).each do |attr| value = self.send(attr) self.send("#{attr}=", Sanitize.clean(value)) if value.present? end end + def set_notification_email + if self.notification_email.blank? || !self.all_emails.include?(self.notification_email) + self.notification_email = self.email + end + end + + def set_public_email + if self.public_email.blank? || !self.all_emails.include?(self.public_email) + self.public_email = nil + end + end + + def update_secondary_emails! + self.set_notification_email + self.set_public_email + self.save if self.notification_email_changed? || self.public_email_changed? + end + + def set_projects_limit + connection_default_value_defined = new_record? && !projects_limit_changed? + return unless self.projects_limit.nil? || connection_default_value_defined + + self.projects_limit = current_application_settings.default_projects_limit + end + def requires_ldap_check? if !Gitlab.config.ldap.enabled false @@ -469,13 +594,13 @@ class User < ActiveRecord::Base end def full_website_url - return "http://#{website_url}" if website_url !~ /^https?:\/\// + return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\// website_url end def short_website_url - website_url.gsub(/https?:\/\//, '') + website_url.sub(/\Ahttps?:\/\//, '') end def all_ssh_keys @@ -483,7 +608,7 @@ class User < ActiveRecord::Base end def temp_oauth_email? - email =~ /\Atemp-email-for-oauth/ + email.start_with?('temp-email-for-oauth') end def public_profile? @@ -492,12 +617,16 @@ class User < ActiveRecord::Base def avatar_url(size = nil) if avatar.present? - [gitlab_config.url, avatar.url].join("/") + [gitlab_config.url, avatar.url].join else GravatarService.new.execute(email, size) end end + def all_emails + [self.email, *self.emails.map(&:email)] + end + def hook_attrs { name: name, @@ -517,7 +646,7 @@ class User < ActiveRecord::Base def post_create_hook log_info("User \"#{self.name}\" (#{self.email}) was created") - notification_service.new_user(self, @reset_token) + notification_service.new_user(self, @reset_token) if self.created_by_id system_hook_service.execute_hooks_for(self, :create) end @@ -561,4 +690,49 @@ class User < ActiveRecord::Base namespaces += masters_groups end end + + def namespaces + namespace_ids = groups.pluck(:id) + namespace_ids.push(namespace.id) + Namespace.where(id: namespace_ids) + end + + def oauth_authorized_tokens + Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) + end + + def contributed_projects_ids + Event.contributions.where(author_id: self). + where("created_at > ?", Time.now - 1.year). + reorder(project_id: :desc). + select(:project_id). + uniq.map(&:project_id) + end + + def restricted_signup_domains + email_domains = current_application_settings.restricted_signup_domains + + unless email_domains.blank? + match_found = email_domains.any? do |domain| + escaped = Regexp.escape(domain).gsub('\*','.*?') + regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE + email_domain = Mail::Address.new(self.email).domain + email_domain =~ regexp + end + + unless match_found + self.errors.add :email, + 'is not whitelisted. ' + + 'Email domains valid for registration are: ' + + email_domains.join(', ') + return false + end + end + + true + end + + def can_be_removed? + !solo_owned_groups.present? + end end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index b9ab6702c53..e9413c34bae 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -43,7 +43,7 @@ class WikiPage @attributes[:slug] end - alias :to_param :slug + alias_method :to_param, :slug # The formatted title of this page. def title @@ -179,7 +179,8 @@ class WikiPage if valid? && project_wiki.send(method, *args) page_details = if method == :update_page - @page.path + # Use url_path instead of path to omit format extension + @page.url_path else title end diff --git a/app/services/archive_repository_service.rb b/app/services/archive_repository_service.rb index 8823f6fdc67..e1b41527d8d 100644 --- a/app/services/archive_repository_service.rb +++ b/app/services/archive_repository_service.rb @@ -1,14 +1,62 @@ class ArchiveRepositoryService - def execute(project, ref, format) - storage_path = Gitlab.config.gitlab.repository_downloads_path + attr_reader :project, :ref, :format - unless File.directory?(storage_path) - FileUtils.mkdir_p(storage_path) + def initialize(project, ref, format) + format ||= 'tar.gz' + @project, @ref, @format = project, ref, format.downcase + end + + def execute(options = {}) + project.repository.clean_old_archives + + raise "No archive file path" unless file_path + + return file_path if archived? + + unless archiving? + RepositoryArchiveWorker.perform_async(project.id, ref, format) end - format ||= 'tar.gz' - repository = project.repository - repository.clean_old_archives - repository.archive_repo(ref, storage_path, format.downcase) + archived = wait_until_archived(options[:timeout] || 5.0) + + file_path if archived + end + + private + + def storage_path + Gitlab.config.gitlab.repository_downloads_path + end + + def file_path + @file_path ||= project.repository.archive_file_path(ref, storage_path, format) + end + + def pid_file_path + @pid_file_path ||= project.repository.archive_pid_file_path(ref, storage_path, format) + end + + def archived? + File.exist?(file_path) + end + + def archiving? + File.exist?(pid_file_path) + end + + def wait_until_archived(timeout = 5.0) + return archived? if timeout == 0.0 + + t1 = Time.now + + begin + sleep 0.1 + + success = archived? + + t2 = Time.now + end until success || t2 - t1 >= timeout + + success end end diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 0d46eeaa18f..6d9ed345914 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -1,4 +1,6 @@ class BaseService + include Gitlab::CurrentSettings + attr_accessor :project, :current_user, :params def initialize(project, user, params = {}) @@ -29,13 +31,31 @@ class BaseService SystemHooksService.new end + # Add an error to the specified model for restricted visibility levels + def deny_visibility_level(model, denied_visibility_level = nil) + denied_visibility_level ||= model.visibility_level + + level_name = 'Unknown' + Gitlab::VisibilityLevel.options.each do |name, level| + level_name = name if level == denied_visibility_level + end + + model.errors.add( + :visibility_level, + "#{level_name} visibility has been restricted by your GitLab administrator" + ) + end + private - def error(message) - { + def error(message, http_status = nil) + result = { message: message, status: :error } + + result[:http_status] = http_status if http_status + result end def success diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 901f67bafb3..cf7ae4345f3 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -17,10 +17,15 @@ class CreateBranchService < BaseService new_branch = repository.find_branch(branch_name) if new_branch - Event.create_ref_event(project, current_user, new_branch, 'add') - return success(new_branch) + push_data = build_push_data(project, current_user, new_branch) + + EventCreateService.new.push(project, current_user, push_data) + project.execute_hooks(push_data.dup, :push_hooks) + project.execute_services(push_data.dup, :push_hooks) + + success(new_branch) else - return error('Invalid reference name') + error('Invalid reference name') end end @@ -29,4 +34,9 @@ class CreateBranchService < BaseService out[:branch] = branch out end + + def build_push_data(project, user, branch) + Gitlab::PushDataBuilder. + build(project, user, Gitlab::Git::BLANK_SHA, branch.target, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) + end end diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb new file mode 100644 index 00000000000..101a3df5eee --- /dev/null +++ b/app/services/create_snippet_service.rb @@ -0,0 +1,20 @@ +class CreateSnippetService < BaseService + def execute + if project.nil? + snippet = PersonalSnippet.new(params) + else + snippet = project.snippets.build(params) + end + + unless Gitlab::VisibilityLevel.allowed_for?(current_user, + params[:visibility_level]) + deny_visibility_level(snippet) + return snippet + end + + snippet.author = current_user + + snippet.save + snippet + end +end diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index 9b2a2270233..1a7318048b3 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -13,18 +13,21 @@ class CreateTagService < BaseService return error('Tag already exists') end - if message - message.gsub!(/^\s+|\s+$/, '') - end + message.strip! if message repository.add_tag(tag_name, ref, message) new_tag = repository.find_tag(tag_name) if new_tag - Event.create_ref_event(project, current_user, new_tag, 'add', 'refs/tags') - return success(new_tag) + push_data = create_push_data(project, current_user, new_tag) + + EventCreateService.new.push(project, current_user, push_data) + project.execute_hooks(push_data.dup, :tag_push_hooks) + project.execute_services(push_data.dup, :tag_push_hooks) + + success(new_tag) else - return error('Invalid reference name') + error('Invalid reference name') end end @@ -33,4 +36,10 @@ class CreateTagService < BaseService out[:tag] = branch out end + + def create_push_data(project, user, tag) + commits = [project.commit(tag.target)].compact + Gitlab::PushDataBuilder. + build(project, user, Gitlab::Git::BLANK_SHA, tag.target, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", commits, tag.message) + end end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index cae6327fe72..b19b112a0c4 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -25,10 +25,15 @@ class DeleteBranchService < BaseService end if repository.rm_branch(branch_name) - Event.create_ref_event(project, current_user, branch, 'rm') + push_data = build_push_data(branch) + + EventCreateService.new.push(project, current_user, push_data) + project.execute_hooks(push_data.dup, :push_hooks) + project.execute_services(push_data.dup, :push_hooks) + success('Branch was removed') else - return error('Failed to remove branch') + error('Failed to remove branch') end end @@ -43,4 +48,9 @@ class DeleteBranchService < BaseService out[:message] = message out end + + def build_push_data(branch) + Gitlab::PushDataBuilder + .build(project, current_user, branch.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) + end end diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb new file mode 100644 index 00000000000..0c836401136 --- /dev/null +++ b/app/services/delete_tag_service.rb @@ -0,0 +1,42 @@ +require_relative 'base_service' + +class DeleteTagService < BaseService + def execute(tag_name) + repository = project.repository + tag = repository.find_tag(tag_name) + + # No such tag + unless tag + return error('No such tag', 404) + end + + if repository.rm_tag(tag_name) + push_data = build_push_data(tag) + + EventCreateService.new.push(project, current_user, push_data) + project.execute_hooks(push_data.dup, :tag_push_hooks) + project.execute_services(push_data.dup, :tag_push_hooks) + + success('Tag was removed') + else + error('Failed to remove tag') + end + end + + def error(message, return_code = 400) + out = super(message) + out[:return_code] = return_code + out + end + + def success(message) + out = super() + out[:message] = message + out + end + + def build_push_data(tag) + Gitlab::PushDataBuilder + .build(project, current_user, tag.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) + end +end diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb new file mode 100644 index 00000000000..9017a63af3b --- /dev/null +++ b/app/services/delete_user_service.rb @@ -0,0 +1,16 @@ +class DeleteUserService + def execute(user) + if user.solo_owned_groups.present? + user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user' + user + else + user.personal_projects.each do |project| + # Skip repository removal because we remove directory with namespace + # that contain all this repositories + ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + end + + user.destroy + end + end +end diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb new file mode 100644 index 00000000000..d929a676293 --- /dev/null +++ b/app/services/destroy_group_service.rb @@ -0,0 +1,17 @@ +class DestroyGroupService + attr_accessor :group, :current_user + + def initialize(group, user) + @group, @current_user = group, user + end + + def execute + @group.projects.each do |project| + # Skip repository removal because we remove directory with namespace + # that contain all this repositories + ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + end + + @group.destroy + end +end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 8d8a5873e62..103d6b0a08b 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -7,58 +7,78 @@ # class EventCreateService def open_issue(issue, current_user) - create_event(issue, current_user, Event::CREATED) + create_record_event(issue, current_user, Event::CREATED) end def close_issue(issue, current_user) - create_event(issue, current_user, Event::CLOSED) + create_record_event(issue, current_user, Event::CLOSED) end def reopen_issue(issue, current_user) - create_event(issue, current_user, Event::REOPENED) + create_record_event(issue, current_user, Event::REOPENED) end def open_mr(merge_request, current_user) - create_event(merge_request, current_user, Event::CREATED) + create_record_event(merge_request, current_user, Event::CREATED) end def close_mr(merge_request, current_user) - create_event(merge_request, current_user, Event::CLOSED) + create_record_event(merge_request, current_user, Event::CLOSED) end def reopen_mr(merge_request, current_user) - create_event(merge_request, current_user, Event::REOPENED) + create_record_event(merge_request, current_user, Event::REOPENED) end def merge_mr(merge_request, current_user) - create_event(merge_request, current_user, Event::MERGED) + create_record_event(merge_request, current_user, Event::MERGED) end def open_milestone(milestone, current_user) - create_event(milestone, current_user, Event::CREATED) + create_record_event(milestone, current_user, Event::CREATED) end def close_milestone(milestone, current_user) - create_event(milestone, current_user, Event::CLOSED) + create_record_event(milestone, current_user, Event::CLOSED) end def reopen_milestone(milestone, current_user) - create_event(milestone, current_user, Event::REOPENED) + create_record_event(milestone, current_user, Event::REOPENED) end def leave_note(note, current_user) - create_event(note, current_user, Event::COMMENTED) + create_record_event(note, current_user, Event::COMMENTED) + end + + def join_project(project, current_user) + create_event(project, current_user, Event::JOINED) + end + + def leave_project(project, current_user) + create_event(project, current_user, Event::LEFT) + end + + def create_project(project, current_user) + create_event(project, current_user, Event::CREATED) + end + + def push(project, current_user, push_data) + create_event(project, current_user, Event::PUSHED, data: push_data) end private - def create_event(record, current_user, status) - Event.create( - project: record.project, - target_id: record.id, - target_type: record.class.name, + def create_record_event(record, current_user, status) + create_event(record.project, current_user, status, target_id: record.id, target_type: record.class.name) + end + + def create_event(project, current_user, status, attributes = {}) + attributes.reverse_merge!( + project: project, action: status, author_id: current_user.id ) + + Event.create(attributes) end end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index bd245100955..f587ee266da 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -1,11 +1,34 @@ module Files class BaseService < ::BaseService - attr_reader :ref, :path + class ValidationError < StandardError; end - def initialize(project, user, params, ref, path = nil) - @project, @current_user, @params = project, user, params.dup - @ref = ref - @path = path + def execute + @current_branch = params[:current_branch] + @target_branch = params[:target_branch] + @commit_message = params[:commit_message] + @file_path = params[:file_path] + @file_content = if params[:file_content_encoding] == 'base64' + Base64.decode64(params[:file_content]) + else + params[:file_content] + end + + # Validate parameters + validate + + # Create new branch if it different from current_branch + if @target_branch != @current_branch + create_target_branch + end + + if sha = commit + after_commit(sha, @target_branch) + success + else + error("Something went wrong. Your changes were not committed") + end + rescue ValidationError => ex + error(ex.message) end private @@ -13,5 +36,52 @@ module Files def repository project.repository end + + def after_commit(sha, branch) + commit = repository.commit(sha) + full_ref = 'refs/heads/' + branch + old_sha = commit.parent_id || Gitlab::Git::BLANK_SHA + GitPushService.new.execute(project, current_user, old_sha, sha, full_ref) + end + + def current_branch + @current_branch ||= params[:current_branch] + end + + def target_branch + @target_branch ||= params[:target_branch] + end + + def raise_error(message) + raise ValidationError.new(message) + end + + def validate + allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch) + + unless allowed + raise_error("You are not allowed to push into this branch") + end + + unless project.empty_repo? + unless repository.branch_names.include?(@current_branch) + raise_error("You can only create files if you are on top of a branch") + end + + if @current_branch != @target_branch + if repository.branch_names.include?(@target_branch) + raise_error("Branch with such name already exists. You need to switch to this branch in order to make changes") + end + end + end + end + + def create_target_branch + result = CreateBranchService.new(project, current_user).execute(@target_branch, @current_branch) + + unless result[:status] == :success + raise_error("Something went wrong when we tried to create #{@target_branch} for you") + end + end end end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 82e4d7b684f..91d715b2d63 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -1,49 +1,29 @@ require_relative "base_service" module Files - class CreateService < BaseService - def execute - allowed = if project.protected_branch?(ref) - can?(current_user, :push_code_to_protected_branches, project) - else - can?(current_user, :push_code, project) - end - - unless allowed - return error("You are not allowed to create file in this branch") - end + class CreateService < Files::BaseService + def commit + repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch) + end - unless repository.branch_names.include?(ref) - return error("You can only create files if you are on top of a branch") - end + def validate + super - file_name = File.basename(path) - file_path = path + file_name = File.basename(@file_path) - unless file_name =~ Gitlab::Regex.path_regex - return error( + unless file_name =~ Gitlab::Regex.file_name_regex + raise_error( 'Your changes could not be committed, because the file name ' + - Gitlab::Regex.path_regex_message + Gitlab::Regex.file_name_regex_message ) end - blob = repository.blob_at_branch(ref, file_path) - - if blob - return error("Your changes could not be committed, because file with such name exists") - end - - new_file_action = Gitlab::Satellite::NewFileAction.new(current_user, project, ref, file_path) - created_successfully = new_file_action.commit!( - params[:content], - params[:commit_message], - params[:encoding] - ) + unless project.empty_repo? + blob = repository.blob_at_branch(@current_branch, @file_path) - if created_successfully - success - else - error("Your changes could not be committed, because the file has been changed") + if blob + raise_error("Your changes could not be committed, because file with such name exists") + end end end end diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index ff5dc6ef34c..27c881c3430 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -1,40 +1,9 @@ require_relative "base_service" module Files - class DeleteService < BaseService - def execute - allowed = if project.protected_branch?(ref) - can?(current_user, :push_code_to_protected_branches, project) - else - can?(current_user, :push_code, project) - end - - unless allowed - return error("You are not allowed to push into this branch") - end - - unless repository.branch_names.include?(ref) - return error("You can only create files if you are on top of a branch") - end - - blob = repository.blob_at_branch(ref, path) - - unless blob - return error("You can only edit text files") - end - - delete_file_action = Gitlab::Satellite::DeleteFileAction.new(current_user, project, ref, path) - - deleted_successfully = delete_file_action.commit!( - nil, - params[:commit_message] - ) - - if deleted_successfully - success - else - error("Your changes could not be committed, because the file has been changed") - end + class DeleteService < Files::BaseService + def commit + repository.remove_file(current_user, @file_path, @commit_message, @target_branch) end end end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index a0f40154db0..a20903c6f02 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -1,40 +1,9 @@ require_relative "base_service" module Files - class UpdateService < BaseService - def execute - allowed = if project.protected_branch?(ref) - can?(current_user, :push_code_to_protected_branches, project) - else - can?(current_user, :push_code, project) - end - - unless allowed - return error("You are not allowed to push into this branch") - end - - unless repository.branch_names.include?(ref) - return error("You can only create files if you are on top of a branch") - end - - blob = repository.blob_at_branch(ref, path) - - unless blob - return error("You can only edit text files") - end - - edit_file_action = Gitlab::Satellite::EditFileAction.new(current_user, project, ref, path) - created_successfully = edit_file_action.commit!( - params[:content], - params[:commit_message], - params[:encoding] - ) - - if created_successfully - success - else - error("Your changes could not be committed. Maybe the file was changed by another process or there was nothing to commit?") - end + class UpdateService < Files::BaseService + def commit + repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch) end end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 529af1970f6..6135ae65007 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -1,5 +1,7 @@ class GitPushService attr_accessor :project, :user, :push_data, :push_commits + include Gitlab::CurrentSettings + include Gitlab::Access # This method will be called after each git update # and only if the provided user and project is present in GitLab. @@ -21,58 +23,48 @@ class GitPushService project.repository.expire_cache project.update_repository_size - if push_to_branch?(ref) - if push_remove_branch?(ref, newrev) - @push_commits = [] - elsif push_to_new_branch?(ref, oldrev) - # Re-find the pushed commits. - if is_default_branch?(ref) - # Initial push to the default branch. Take the full history of that branch as "newly pushed". - @push_commits = project.repository.commits(newrev) - # Default branch is protected by default - project.protected_branches.create({ name: project.default_branch }) - else - # Use the pushed commits that aren't reachable by the default branch - # as a heuristic. This may include more commits than are actually pushed, but - # that shouldn't matter because we check for existing cross-references later. - @push_commits = project.repository.commits_between(project.default_branch, newrev) + if push_remove_branch?(ref, newrev) + @push_commits = [] + elsif push_to_new_branch?(ref, oldrev) + # Re-find the pushed commits. + if is_default_branch?(ref) + # Initial push to the default branch. Take the full history of that branch as "newly pushed". + @push_commits = project.repository.commits(newrev) + + # Ensure HEAD points to the default branch in case it is not master + branch_name = Gitlab::Git.ref_name(ref) + project.change_head(branch_name) + + # Set protection on the default branch if configured + if (current_application_settings.default_branch_protection != PROTECTION_NONE) + developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false + project.protected_branches.create({ name: project.default_branch, developers_can_push: developers_can_push }) end - process_commit_messages(ref) - elsif push_to_existing_branch?(ref, oldrev) - # Collect data for this git push - @push_commits = project.repository.commits_between(oldrev, newrev) - project.update_merge_requests(oldrev, newrev, ref, @user) + else + # Use the pushed commits that aren't reachable by the default branch + # as a heuristic. This may include more commits than are actually pushed, but + # that shouldn't matter because we check for existing cross-references later. + @push_commits = project.repository.commits_between(project.default_branch, newrev) + + # don't process commits for the initial push to the default branch process_commit_messages(ref) end - - @push_data = post_receive_data(oldrev, newrev, ref) - create_push_event(@push_data) - project.execute_hooks(@push_data.dup, :push_hooks) - project.execute_services(@push_data.dup) + elsif push_to_existing_branch?(ref, oldrev) + # Collect data for this git push + @push_commits = project.repository.commits_between(oldrev, newrev) + project.update_merge_requests(oldrev, newrev, ref, @user) + process_commit_messages(ref) end - end - # This method provide a sample data - # generated with post_receive_data method - # for given project - # - def sample_data(project, user) - @project, @user = project, user - @push_commits = project.repository.commits(project.default_branch, nil, 3) - post_receive_data(@push_commits.last.id, @push_commits.first.id, "refs/heads/#{project.default_branch}") + @push_data = build_push_data(oldrev, newrev, ref) + + EventCreateService.new.push(project, user, @push_data) + project.execute_hooks(@push_data.dup, :push_hooks) + project.execute_services(@push_data.dup, :push_hooks) end protected - def create_push_event(push_data) - Event.create!( - project: project, - action: Event::PUSHED, - data: push_data, - author_id: push_data[:user_id] - ) - end - # Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched, # close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables. def process_commit_messages(ref) @@ -82,7 +74,7 @@ class GitPushService # Close issues if these commits were pushed to the project's default branch and the commit message matches the # closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to # a different branch. - issues_to_close = commit.closes_issues(project) + issues_to_close = commit.closes_issues(user) # Load commit author only if needed. # For push with 1k commits it prevents 900+ requests in database @@ -96,104 +88,56 @@ class GitPushService end end - # Create cross-reference notes for any other references. Omit any issues that were referenced in an - # issue-closing phrase, or have already been mentioned from this commit (probably from this commit - # being pushed to a different branch). - refs = commit.references(project) - issues_to_close - refs.reject! { |r| commit.has_mentioned?(r) } - - if refs.present? - author ||= commit_user(commit) - - refs.each do |r| - Note.create_cross_reference_note(r, commit, author, project) - end + if project.default_issues_tracker? + create_cross_reference_notes(commit, issues_to_close) end end end - # Produce a hash of post-receive data - # - # data = { - # before: String, - # after: String, - # ref: String, - # user_id: String, - # user_name: String, - # project_id: String, - # repository: { - # name: String, - # url: String, - # description: String, - # homepage: String, - # }, - # commits: Array, - # total_commits_count: Fixnum - # } - # - def post_receive_data(oldrev, newrev, ref) - # Total commits count - push_commits_count = push_commits.size - - # Get latest 20 commits ASC - push_commits_limited = push_commits.last(20) - - # Hash to be passed as post_receive_data - data = { - before: oldrev, - after: newrev, - ref: ref, - user_id: user.id, - user_name: user.name, - project_id: project.id, - repository: { - name: project.name, - url: project.url_to_repo, - description: project.description, - homepage: project.web_url, - }, - commits: [], - total_commits_count: push_commits_count - } - - # For performance purposes maximum 20 latest commits - # will be passed as post receive hook data. - # - push_commits_limited.each do |commit| - data[:commits] << commit.hook_attrs(project) + def create_cross_reference_notes(commit, issues_to_close) + # Create cross-reference notes for any other references. Omit any issues that were referenced in an + # issue-closing phrase, or have already been mentioned from this commit (probably from this commit + # being pushed to a different branch). + refs = commit.references(project, user) - issues_to_close + refs.reject! { |r| commit.has_mentioned?(r) } + + if refs.present? + author ||= commit_user(commit) + + refs.each do |r| + SystemNoteService.cross_reference(r, commit, author) + end end + end - data + def build_push_data(oldrev, newrev, ref) + Gitlab::PushDataBuilder. + build(project, user, oldrev, newrev, ref, push_commits) end def push_to_existing_branch?(ref, oldrev) - ref_parts = ref.split('/') - # Return if this is not a push to a branch (e.g. new commits) - ref_parts[1] =~ /heads/ && oldrev != Gitlab::Git::BLANK_SHA + Gitlab::Git.branch_ref?(ref) && !Gitlab::Git.blank_ref?(oldrev) end def push_to_new_branch?(ref, oldrev) - ref_parts = ref.split('/') - - ref_parts[1] =~ /heads/ && oldrev == Gitlab::Git::BLANK_SHA + Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(oldrev) end def push_remove_branch?(ref, newrev) - ref_parts = ref.split('/') - - ref_parts[1] =~ /heads/ && newrev == Gitlab::Git::BLANK_SHA + Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(newrev) end def push_to_branch?(ref) - ref =~ /refs\/heads/ + Gitlab::Git.branch_ref?(ref) end def is_default_branch?(ref) - ref == "refs/heads/#{project.default_branch}" + Gitlab::Git.branch_ref?(ref) && + (Gitlab::Git.ref_name(ref) == project.default_branch || project.default_branch.nil?) end def commit_user(commit) - User.find_for_commit(commit.author_email, commit.author_name) || user + commit.author || user end end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 62eaf9b4f51..075a6118da2 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -3,38 +3,35 @@ class GitTagPushService def execute(project, user, oldrev, newrev, ref) @project, @user = project, user - @push_data = create_push_data(oldrev, newrev, ref) - create_push_event - project.repository.expire_cache + @push_data = build_push_data(oldrev, newrev, ref) + + EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :tag_push_hooks) + project.execute_services(@push_data.dup, :tag_push_hooks) + + project.repository.expire_cache + + true end private - def create_push_data(oldrev, newrev, ref) - data = { - ref: ref, - before: oldrev, - after: newrev, - user_id: user.id, - user_name: user.name, - project_id: project.id, - repository: { - name: project.name, - url: project.url_to_repo, - description: project.description, - homepage: project.web_url - } - } - end + def build_push_data(oldrev, newrev, ref) + commits = [] + message = nil + + if !Gitlab::Git.blank_ref?(newrev) + tag_name = Gitlab::Git.ref_name(ref) + tag = project.repository.find_tag(tag_name) + if tag && tag.target == newrev + commit = project.commit(tag.target) + commits = [commit].compact + message = tag.message + end + end - def create_push_event - Event.create!( - project: project, - action: Event::PUSHED, - data: push_data, - author_id: push_data[:user_id] - ) + Gitlab::PushDataBuilder. + build(project, user, oldrev, newrev, ref, commits, message) end end diff --git a/app/services/gravatar_service.rb b/app/services/gravatar_service.rb index a69c7c78377..4bee0c26a68 100644 --- a/app/services/gravatar_service.rb +++ b/app/services/gravatar_service.rb @@ -1,6 +1,8 @@ class GravatarService + include Gitlab::CurrentSettings + def execute(email, size = nil) - if gravatar_config.enabled && email.present? + if current_application_settings.gravatar_enabled? && email.present? size = 40 if size.nil? || size <= 0 sprintf gravatar_url, diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index e3371ec3c1b..1d99223cfe6 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -2,12 +2,28 @@ class IssuableBaseService < BaseService private def create_assignee_note(issuable) - Note.create_assignee_change_note( + SystemNoteService.change_assignee( issuable, issuable.project, current_user, issuable.assignee) end def create_milestone_note(issuable) - Note.create_milestone_change_note( + SystemNoteService.change_milestone( issuable, issuable.project, current_user, issuable.milestone) end + + def create_labels_note(issuable, added_labels, removed_labels) + SystemNoteService.change_label( + issuable, issuable.project, current_user, added_labels, removed_labels) + end + + def create_title_change_note(issuable, old_title) + SystemNoteService.change_title( + issuable, issuable.project, current_user, old_title) + end + + def create_branch_change_note(issuable, branch_type, old_branch, new_branch) + SystemNoteService.change_branch( + issuable, issuable.project, current_user, branch_type, + old_branch, new_branch) + end end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 755c0ef45a8..c3ca04a4343 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -1,13 +1,19 @@ module Issues class BaseService < ::IssuableBaseService - private - - def execute_hooks(issue, action = 'open') + def hook_data(issue, action) issue_data = issue.to_hook_data(current_user) issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id) issue_data[:object_attributes].merge!(url: issue_url, action: action) + issue_data + end + + private + + def execute_hooks(issue, action = 'open') + issue_data = hook_data(issue, action) issue.project.execute_hooks(issue_data, :issue_hooks) + issue.project.execute_services(issue_data, :issue_hooks) end end end diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb index f72a346af6f..eb07413ee94 100644 --- a/app/services/issues/bulk_update_service.rb +++ b/app/services/issues/bulk_update_service.rb @@ -1,38 +1,23 @@ module Issues class BulkUpdateService < BaseService def execute - update_data = params[:update] + issues_ids = params.delete(:issues_ids).split(",") + issue_params = params - issues_ids = update_data[:issues_ids].split(",") - milestone_id = update_data[:milestone_id] - assignee_id = update_data[:assignee_id] - status = update_data[:status] - - new_state = nil - - if status.present? - if status == 'closed' - new_state = :close - else - new_state = :reopen - end - end - - opts = {} - opts[:milestone_id] = milestone_id if milestone_id.present? - opts[:assignee_id] = assignee_id if assignee_id.present? + 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? issues = Issue.where(id: issues_ids) - issues = issues.select { |issue| can?(current_user, :modify_issue, issue) } - issues.each do |issue| - issue.update_attributes(opts) - issue.send new_state if new_state + next unless can?(current_user, :modify_issue, issue) + + Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue) end { - count: issues.count, - success: !issues.count.zero? + count: issues.count, + success: !issues.count.zero? } end end diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index ffed13a12e1..3d85f97b7e5 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -1,10 +1,10 @@ module Issues class CloseService < Issues::BaseService def execute(issue, commit = nil) - if issue.close - notification_service.close_issue(issue, current_user) + if project.default_issues_tracker? && issue.close event_service.close_issue(issue, current_user) create_note(issue, commit) + notification_service.close_issue(issue, current_user) execute_hooks(issue, 'close') end @@ -14,7 +14,7 @@ module Issues private def create_note(issue, current_commit) - Note.create_status_change_note(issue, issue.project, current_user, issue.state, current_commit) + SystemNoteService.change_status(issue, issue.project, current_user, issue.state, current_commit) end end end diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb index 1e5c398516d..e48ca359f4f 100644 --- a/app/services/issues/reopen_service.rb +++ b/app/services/issues/reopen_service.rb @@ -14,7 +14,7 @@ module Issues private def create_note(issue) - Note.create_status_change_note(issue, issue.project, current_user, issue.state, nil) + SystemNoteService.change_status(issue, issue.project, current_user, issue.state, nil) end end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 0ee9635ed99..6af942a5ca4 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -14,17 +14,31 @@ module Issues issue.update_nth_task(params[:task_num].to_i, false) end + params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE + params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE + + old_labels = issue.labels.to_a + if params.present? && issue.update_attributes(params.except(:state_event, :task_num)) issue.reset_events_cache + if issue.labels != old_labels + create_labels_note( + issue, issue.labels - old_labels, old_labels - issue.labels) + end + if issue.previous_changes.include?('milestone_id') create_milestone_note(issue) end if issue.previous_changes.include?('assignee_id') - notification_service.reassigned_issue(issue, current_user) create_assignee_note(issue) + notification_service.reassigned_issue(issue, current_user) + end + + if issue.previous_changes.include?('title') + create_title_change_note(issue, issue.previous_changes['title'].first) end issue.notice_added_references(issue.project, current_user) diff --git a/app/services/merge_requests/auto_merge_service.rb b/app/services/merge_requests/auto_merge_service.rb index 20b88d1510c..378b39bb9d6 100644 --- a/app/services/merge_requests/auto_merge_service.rb +++ b/app/services/merge_requests/auto_merge_service.rb @@ -5,15 +5,16 @@ module MergeRequests # mark merge request as merged and execute all hooks and notifications # Called when you do merge via GitLab UI class AutoMergeService < BaseMergeService - def execute(merge_request, current_user, commit_message) + def execute(merge_request, commit_message) merge_request.lock_mr if Gitlab::Satellite::MergeAction.new(current_user, merge_request).merge!(commit_message) merge_request.merge - notification.merge_mr(merge_request, current_user) create_merge_event(merge_request, current_user) - execute_project_hooks(merge_request) + create_note(merge_request) + notification_service.merge_mr(merge_request, current_user) + execute_hooks(merge_request) true else diff --git a/app/services/merge_requests/base_merge_service.rb b/app/services/merge_requests/base_merge_service.rb index 700a21ca011..9579573adf9 100644 --- a/app/services/merge_requests/base_merge_service.rb +++ b/app/services/merge_requests/base_merge_service.rb @@ -1,21 +1,10 @@ module MergeRequests - class BaseMergeService + class BaseMergeService < MergeRequests::BaseService private - def notification - NotificationService.new - end - def create_merge_event(merge_request, current_user) EventCreateService.new.merge_mr(merge_request, current_user) end - - def execute_project_hooks(merge_request) - if merge_request.project - hook_data = merge_request.to_hook_data(current_user) - merge_request.project.execute_hooks(hook_data, :merge_request_hooks) - end - end end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 7f3421b8e4b..e455fe95791 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -2,13 +2,22 @@ module MergeRequests class BaseService < ::IssuableBaseService def create_note(merge_request) - Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil) + SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil) end - def execute_hooks(merge_request) + def hook_data(merge_request, action) + hook_data = merge_request.to_hook_data(current_user) + merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) + hook_data[:object_attributes][:url] = merge_request_url + hook_data[:object_attributes][:action] = action + hook_data + end + + def execute_hooks(merge_request, action = 'open') if merge_request.project - hook_data = merge_request.to_hook_data(current_user) - merge_request.project.execute_hooks(hook_data, :merge_request_hooks) + merge_data = hook_data(merge_request, action) + merge_request.project.execute_hooks(merge_data, :merge_request_hooks) + merge_request.project.execute_services(merge_data, :merge_request_hooks) end end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 1475973e543..956480938c3 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -13,12 +13,9 @@ module MergeRequests merge_request.target_branch ||= merge_request.target_project.default_branch unless merge_request.target_branch && merge_request.source_branch - return build_failed(merge_request, "You must select source and target branches") + return build_failed(merge_request, nil) end - # Generate suggested MR title based on source branch name - merge_request.title = merge_request.source_branch.titleize.humanize - compare_result = CompareService.new.execute( current_user, merge_request.source_project, @@ -32,7 +29,7 @@ module MergeRequests # At this point we decide if merge request can be created # If we have at least one commit to merge -> creation allowed if commits.present? - merge_request.compare_commits = Commit.decorate(commits) + merge_request.compare_commits = Commit.decorate(commits, merge_request.source_project) merge_request.can_be_created = true merge_request.compare_failed = false @@ -52,6 +49,15 @@ module MergeRequests merge_request.compare_failed = false end + commits = merge_request.compare_commits + if commits && commits.count == 1 + commit = commits.first + merge_request.title = commit.title + merge_request.description = commit.description.try(:strip) + else + merge_request.title = merge_request.source_branch.titleize.humanize + end + merge_request rescue Gitlab::Satellite::BranchesWithoutParent @@ -59,7 +65,7 @@ module MergeRequests end def build_failed(merge_request, message) - merge_request.errors.add(:base, message) + merge_request.errors.add(:base, message) unless message.nil? merge_request.compare_commits = [] merge_request.can_be_created = false merge_request diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index 64e37a23e6b..47454f9f0c2 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -7,9 +7,9 @@ module MergeRequests if merge_request.close event_service.close_mr(merge_request, current_user) - notification_service.close_mr(merge_request, current_user) create_note(merge_request) - execute_hooks(merge_request) + notification_service.close_mr(merge_request, current_user) + execute_hooks(merge_request, 'close') end merge_request diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 680766140bd..327ead4ff3f 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -6,12 +6,13 @@ module MergeRequests # Called when you do merge via command line and push code # to target branch class MergeService < BaseMergeService - def execute(merge_request, current_user, commit_message) + def execute(merge_request, commit_message) merge_request.merge - notification.merge_mr(merge_request, current_user) create_merge_event(merge_request, current_user) - execute_project_hooks(merge_request) + create_note(merge_request) + notification_service.merge_mr(merge_request, current_user) + execute_hooks(merge_request, 'merge') true rescue diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index baf0936cc3d..d0648da049b 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -1,15 +1,16 @@ module MergeRequests class RefreshService < MergeRequests::BaseService def execute(oldrev, newrev, ref) - return true unless ref =~ /heads/ + return true unless Gitlab::Git.branch_ref?(ref) @oldrev, @newrev = oldrev, newrev - @branch_name = ref.gsub("refs/heads/", "") + @branch_name = Gitlab::Git.ref_name(ref) @fork_merge_requests = @project.fork_merge_requests.opened @commits = @project.repository.commits_between(oldrev, newrev) close_merge_requests reload_merge_requests + execute_mr_web_hooks comment_mr_with_commits true @@ -32,7 +33,9 @@ module MergeRequests merge_requests.uniq.select(&:source_project).each do |merge_request| - MergeRequests::MergeService.new.execute(merge_request, @current_user, nil) + MergeRequests::MergeService. + new(merge_request.target_project, @current_user). + execute(merge_request, nil) end end @@ -74,8 +77,29 @@ module MergeRequests merge_requests = filter_merge_requests(merge_requests) merge_requests.each do |merge_request| - Note.create_new_commits_note(merge_request, merge_request.project, - @current_user, @commits) + mr_commit_ids = Set.new(merge_request.commits.map(&:id)) + + new_commits, existing_commits = @commits.partition do |commit| + mr_commit_ids.include?(commit.id) + end + + SystemNoteService.add_commits(merge_request, merge_request.project, + @current_user, new_commits, + existing_commits, @oldrev) + end + end + + # Call merge request webhook with update branches + def execute_mr_web_hooks + merge_requests = @project.origin_merge_requests.opened + .where(source_branch: @branch_name) + .to_a + merge_requests += @fork_merge_requests.where(source_branch: @branch_name) + .to_a + merge_requests = filter_merge_requests(merge_requests) + + merge_requests.each do |merge_request| + execute_hooks(merge_request, 'update') end end diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb index bd68919a550..8279ad2001b 100644 --- a/app/services/merge_requests/reopen_service.rb +++ b/app/services/merge_requests/reopen_service.rb @@ -3,9 +3,9 @@ module MergeRequests def execute(merge_request) if merge_request.reopen event_service.reopen_mr(merge_request, current_user) - notification_service.reopen_mr(merge_request, current_user) create_note(merge_request) - execute_hooks(merge_request) + notification_service.reopen_mr(merge_request, current_user) + execute_hooks(merge_request, 'reopen') merge_request.reload_code merge_request.mark_as_unchecked end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index fc26619cd17..4f6c6cba9a9 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -5,10 +5,11 @@ require_relative 'close_service' module MergeRequests class UpdateService < MergeRequests::BaseService def execute(merge_request) - # We dont allow change of source/target projects + # We don't allow change of source/target projects and source branch # after merge request was created params.except!(:source_project_id) params.except!(:target_project_id) + params.except!(:source_branch) state = params[:state_event] @@ -23,22 +24,50 @@ module MergeRequests merge_request.update_nth_task(params[:task_num].to_i, false) end + params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE + params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE + + old_labels = merge_request.labels.to_a + if params.present? && merge_request.update_attributes( params.except(:state_event, :task_num) ) merge_request.reset_events_cache + if merge_request.labels != old_labels + create_labels_note( + merge_request, + merge_request.labels - old_labels, + old_labels - merge_request.labels + ) + end + + if merge_request.previous_changes.include?('target_branch') + create_branch_change_note(merge_request, 'target', + merge_request.previous_changes['target_branch'].first, + merge_request.target_branch) + end + if merge_request.previous_changes.include?('milestone_id') create_milestone_note(merge_request) end if merge_request.previous_changes.include?('assignee_id') - notification_service.reassigned_merge_request(merge_request, current_user) create_assignee_note(merge_request) + notification_service.reassigned_merge_request(merge_request, current_user) + end + + if merge_request.previous_changes.include?('title') + create_title_change_note(merge_request, merge_request.previous_changes['title'].first) + end + + if merge_request.previous_changes.include?('target_branch') || + merge_request.previous_changes.include?('source_branch') + merge_request.mark_as_unchecked end merge_request.notice_added_references(merge_request.project, current_user) - execute_hooks(merge_request) + execute_hooks(merge_request, 'update') end merge_request diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index f64006a4edc..482c0444049 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -15,12 +15,24 @@ module Notes # Create a cross-reference note if this Note contains GFM that names an # issue, merge request, or commit. note.references.each do |mentioned| - Note.create_cross_reference_note(mentioned, note.noteable, note.author, note.project) + SystemNoteService.cross_reference(mentioned, note.noteable, note.author) end + + execute_hooks(note) end end note end + + def hook_data(note) + Gitlab::NoteDataBuilder.build(note, current_user) + end + + def execute_hooks(note) + note_data = hook_data(note) + note.project.execute_hooks(note_data, :note_hooks) + note.project.execute_services(note_data, :note_hooks) + end end end diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb new file mode 100644 index 00000000000..b5611d46257 --- /dev/null +++ b/app/services/notes/update_service.rb @@ -0,0 +1,24 @@ +module Notes + class UpdateService < BaseService + def execute + note = project.notes.find(params[:note_id]) + note.note = params[:note] + if note.save + notification_service.new_note(note) + + # Skip system notes, like status changes and cross-references. + unless note.system + event_service.leave_note(note, note.author) + + # Create a cross-reference note if this Note contains GFM that + # names an issue, merge request, or commit. + note.references.each do |mentioned| + SystemNoteService.cross_reference(mentioned, note.noteable, note.author) + end + end + end + + note + end + end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 2b6217e2e29..312b56eb87b 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -91,8 +91,14 @@ class NotificationService # * project team members with notification level higher then Participating # def merge_mr(merge_request, current_user) - recipients = reject_muted_users([merge_request.author, merge_request.assignee], merge_request.target_project) - recipients = recipients.concat(project_watchers(merge_request.target_project)).uniq + recipients = [merge_request.author, merge_request.assignee] + + recipients = add_project_watchers(recipients, merge_request.target_project) + recipients = reject_muted_users(recipients, merge_request.target_project) + + recipients = add_subscribed_users(recipients, merge_request) + recipients = reject_unsubscribed_users(recipients, merge_request) + recipients.delete(current_user) recipients.each do |recipient| @@ -107,7 +113,7 @@ class NotificationService # Notify new user with email after creation def new_user(user, token = nil) # Don't email omniauth created users - mailer.new_user_email(user.id, token) unless user.extern_uid? + mailer.new_user_email(user.id, token) unless user.identities.any? end # Notify users on new note in system @@ -118,36 +124,34 @@ class NotificationService return true unless note.noteable_type.present? # ignore gitlab service messages - return true if note.note =~ /\A_Status changed to closed_/ + return true if note.note.start_with?('Status changed to closed') return true if note.cross_reference? && note.system == true - opts = { noteable_type: note.noteable_type, project_id: note.project_id } - target = note.noteable - if target.respond_to?(:participants) - recipients = target.participants - else - recipients = note.mentioned_users - end + recipients = [] - if note.commit_id.present? - opts.merge!(commit_id: note.commit_id) - recipients << note.commit_author - else - opts.merge!(noteable_id: note.noteable_id) - end - - # Get users who left comment in thread - recipients = recipients.concat(User.where(id: Note.where(opts).pluck(:author_id))) + # Add all users participating in the thread (author, assignee, comment authors) + participants = + if target.respond_to?(:participants) + target.participants(note.author) + else + note.mentioned_users + end + recipients = recipients.concat(participants) # Merge project watchers - recipients = recipients.concat(project_watchers(note.project)).compact.uniq + recipients = add_project_watchers(recipients, note.project) + + # Reject users with Mention notification level, except those mentioned in _this_ note. + recipients = reject_mention_users(recipients - note.mentioned_users, note.project) + recipients = recipients + note.mentioned_users - # Reject mutes users recipients = reject_muted_users(recipients, note.project) - # Reject author + recipients = add_subscribed_users(recipients, note.noteable) + recipients = reject_unsubscribed_users(recipients, note.noteable) + recipients.delete(note.author) # build notify method like 'note_commit_email' @@ -158,20 +162,44 @@ class NotificationService end end - def new_team_member(project_member) + def invite_project_member(project_member, token) + mailer.project_member_invited_email(project_member.id, token) + end + + def accept_project_invite(project_member) + mailer.project_invite_accepted_email(project_member.id) + end + + def decline_project_invite(project_member) + mailer.project_invite_declined_email(project_member.project.id, project_member.invite_email, project_member.access_level, project_member.created_by_id) + end + + def new_project_member(project_member) mailer.project_access_granted_email(project_member.id) end - def update_team_member(project_member) + def update_project_member(project_member) mailer.project_access_granted_email(project_member.id) end - def new_group_member(users_group) - mailer.group_access_granted_email(users_group.id) + def invite_group_member(group_member, token) + mailer.group_member_invited_email(group_member.id, token) + end + + def accept_group_invite(group_member) + mailer.group_invite_accepted_email(group_member.id) + end + + def decline_group_invite(group_member) + mailer.group_invite_declined_email(group_member.group.id, group_member.invite_email, group_member.access_level, group_member.created_by_id) end - def update_group_member(users_group) - mailer.group_access_granted_email(users_group.id) + def new_group_member(group_member) + mailer.group_access_granted_email(group_member.id) + end + + def update_group_member(group_member) + mailer.group_access_granted_email(group_member.id) end def project_was_moved(project) @@ -190,11 +218,11 @@ class NotificationService project_members = project_member_notification(project) users_with_project_level_global = project_member_notification(project, Notification::N_GLOBAL) - users_with_group_level_global = users_group_notification(project, Notification::N_GLOBAL) + users_with_group_level_global = group_member_notification(project, Notification::N_GLOBAL) users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq) users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users) - users_with_group_setting = select_users_group_setting(project, project_members, users_with_group_level_global, users) + users_with_group_setting = select_group_member_setting(project, project_members, users_with_group_level_global, users) User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a end @@ -209,7 +237,7 @@ class NotificationService end end - def users_group_notification(project, notification_level) + def group_member_notification(project, notification_level) if project.group project.group.group_members.where(notification_level: notification_level).pluck(:user_id) else @@ -238,9 +266,9 @@ class NotificationService users end - # Build a list of users based on group notifcation settings - def select_users_group_setting(project, project_members, global_setting, users_global_level_watch) - uids = users_group_notification(project, Notification::N_WATCH) + # Build a list of users based on group notification settings + def select_group_member_setting(project, project_members, global_setting, users_global_level_watch) + uids = group_member_notification(project, Notification::N_WATCH) # Group setting is watch, add to users list if user is not project member users = [] @@ -260,39 +288,83 @@ class NotificationService users end + def add_project_watchers(recipients, project) + recipients.concat(project_watchers(project)).compact.uniq + end + # Remove users with disabled notifications from array # Also remove duplications and nil recipients def reject_muted_users(users, project = nil) users = users.to_a.compact.uniq + users = users.reject(&:blocked?) users.reject do |user| next user.notification.disabled? unless project - tm = project.project_members.find_by(user_id: user.id) + member = project.project_members.find_by(user_id: user.id) - if !tm && project.group - tm = project.group.group_members.find_by(user_id: user.id) + if !member && project.group + member = project.group.group_members.find_by(user_id: user.id) end # reject users who globally disabled notification and has no membership - next user.notification.disabled? unless tm + next user.notification.disabled? unless member # reject users who disabled notification in project - next true if tm.notification.disabled? + next true if member.notification.disabled? # reject users who have N_GLOBAL in project and disabled in global settings - tm.notification.global? && user.notification.disabled? + member.notification.global? && user.notification.disabled? end end - def new_resource_email(target, project, method) - if target.respond_to?(:participants) - recipients = target.participants + # Remove users with notification level 'Mentioned' + def reject_mention_users(users, project = nil) + users = users.to_a.compact.uniq + + users.reject do |user| + next user.notification.mention? unless project + + member = project.project_members.find_by(user_id: user.id) + + if !member && project.group + member = project.group.group_members.find_by(user_id: user.id) + end + + # reject users who globally set mention notification and has no membership + next user.notification.mention? unless member + + # reject users who set mention notification in project + next true if member.notification.mention? + + # reject users who have N_MENTION in project and disabled in global settings + member.notification.global? && user.notification.mention? + end + end + + def reject_unsubscribed_users(recipients, target) + return recipients unless target.respond_to? :subscriptions + + recipients.reject do |user| + subscription = target.subscriptions.find_by_user_id(user.id) + subscription && !subscription.subscribed + end + end + + def add_subscribed_users(recipients, target) + return recipients unless target.respond_to? :subscriptions + + subscriptions = target.subscriptions + + if subscriptions.any? + recipients + subscriptions.where(subscribed: true).map(&:user) else - recipients = [] + recipients end - recipients = reject_muted_users(recipients, project) - recipients = recipients.concat(project_watchers(project)).uniq + end + + def new_resource_email(target, project, method) + recipients = build_recipients(target, project) recipients.delete(target.author) recipients.each do |recipient| @@ -301,8 +373,7 @@ class NotificationService end def close_resource_email(target, project, current_user, method) - recipients = reject_muted_users([target.author, target.assignee], project) - recipients = recipients.concat(project_watchers(project)).uniq + recipients = build_recipients(target, project) recipients.delete(current_user) recipients.each do |recipient| @@ -312,16 +383,7 @@ class NotificationService def reassign_resource_email(target, project, current_user, method) assignee_id_was = previous_record(target, "assignee_id") - - recipients = User.where(id: [target.assignee_id, assignee_id_was]) - - # Add watchers to email list - recipients = recipients.concat(project_watchers(project)) - - # reject users with disabled notifications - recipients = reject_muted_users(recipients, project) - - # Reject me from recipients if I reassign an item + recipients = build_recipients(target, project) recipients.delete(current_user) recipients.each do |recipient| @@ -330,8 +392,7 @@ class NotificationService end def reopen_resource_email(target, project, current_user, method, status) - recipients = reject_muted_users([target.author, target.assignee], project) - recipients = recipients.concat(project_watchers(project)).uniq + recipients = build_recipients(target, project) recipients.delete(current_user) recipients.each do |recipient| @@ -339,6 +400,24 @@ class NotificationService end end + def build_recipients(target, project) + recipients = + if target.respond_to?(:participants) + target.participants + else + [target.author, target.assignee] + end + + recipients = add_project_watchers(recipients, project) + recipients = reject_mention_users(recipients, project) + recipients = reject_muted_users(recipients, project) + + recipients = add_subscribed_users(recipients, target) + recipients = reject_unsubscribed_users(recipients, target) + + recipients + end + def mailer Notify.delay end diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb new file mode 100644 index 00000000000..6194f6ce91e --- /dev/null +++ b/app/services/oauth2/access_token_validation_service.rb @@ -0,0 +1,41 @@ +module Oauth2::AccessTokenValidationService + # Results: + VALID = :valid + EXPIRED = :expired + REVOKED = :revoked + INSUFFICIENT_SCOPE = :insufficient_scope + + class << self + def validate(token, scopes: []) + if token.expired? + return EXPIRED + + elsif token.revoked? + return REVOKED + + elsif !self.sufficient_scope?(token, scopes) + return INSUFFICIENT_SCOPE + + else + return VALID + end + 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) + if scopes.blank? + # if no any scopes required, the scopes of token is sufficient. + return true + else + # If there are scopes required, then check whether + # the set of authorized scopes is a superset of the set of required scopes + required_scopes = Set.new(scopes) + authorized_scopes = Set.new(token.scopes) + + return authorized_scopes >= required_scopes + end + end + end +end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb new file mode 100644 index 00000000000..7408e09ed1e --- /dev/null +++ b/app/services/projects/autocomplete_service.rb @@ -0,0 +1,15 @@ +module Projects + class AutocompleteService < BaseService + def initialize(project) + @project = project + end + + def issues + @project.issues.opened.select([:iid, :title]) + end + + def merge_requests + @project.merge_requests.opened.select([:iid, :title]) + end + end +end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 3672b623806..011f6f6145e 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -5,19 +5,29 @@ module Projects end def execute + forked_from_project_id = params.delete(:forked_from_project_id) + @project = Project.new(params) - # Reset visibility levet if is not allowed to set it - unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) - @project.visibility_level = default_features.visibility_level + # Make sure that the user is allowed to use the specified visibility + # level + unless Gitlab::VisibilityLevel.allowed_for?(current_user, + params[:visibility_level]) + deny_visibility_level(@project) + return @project end - # Parametrize path for project - # - # Ex. - # 'GitLab HQ'.parameterize => "gitlab-hq" - # - @project.path = @project.name.dup.parameterize unless @project.path.present? + # Set project name from path + if @project.name.present? && @project.path.present? + # if both name and path set - everything is ok + elsif @project.path.present? + # Set project name from path + @project.name = @project.path.dup + elsif @project.name.present? + # For compatibility - set path from name + # TODO: remove this in 8.0 + @project.path = @project.name.dup.parameterize + end # get namespace id namespace_id = params[:namespace_id] @@ -37,23 +47,21 @@ module Projects @project.creator = current_user + if forked_from_project_id + @project.build_forked_project_link(forked_from_project_id: forked_from_project_id) + end + Project.transaction do @project.save - unless @project.import? + if @project.persisted? && !@project.import? unless @project.create_repository raise 'Failed to create repository' end end end - if @project.persisted? - if @project.wiki_enabled? - @project.create_wiki - end - - after_create_actions - end + after_create_actions if @project.persisted? @project rescue => ex @@ -74,10 +82,14 @@ module Projects def after_create_actions log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") + + @project.create_wiki if @project.wiki_enabled? + + event_service.create_project(@project, current_user) system_hook_service.execute_hooks_for(@project, :create) unless @project.group - @project.team << [current_user, :master] + @project.team << [current_user, :master, current_user] end @project.update_column(:last_activity_at, @project.created_at) diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 7e1d753b021..403f419ec50 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -1,28 +1,69 @@ module Projects class DestroyService < BaseService + include Gitlab::ShellAdapter + + class DestroyError < StandardError; end + + DELETED_FLAG = '+deleted' + def execute return false unless can?(current_user, :remove_project, project) project.team.truncate project.repository.expire_cache unless project.empty_repo? - if project.destroy - GitlabShellWorker.perform_async( - :remove_repository, - project.path_with_namespace - ) + repo_path = project.path_with_namespace + wiki_path = repo_path + '.wiki' - GitlabShellWorker.perform_async( - :remove_repository, - project.path_with_namespace + ".wiki" - ) + Project.transaction do + project.destroy! - project.satellite.destroy + unless remove_repository(repo_path) + raise_error('Failed to remove project repository. Please try again or contact administrator') + end - log_info("Project \"#{project.name}\" was removed") - system_hook_service.execute_hooks_for(project, :destroy) - true + unless remove_repository(wiki_path) + raise_error('Failed to remove wiki repository. Please try again or contact administrator') + end end + + project.satellite.destroy + log_info("Project \"#{project.name}\" was removed") + system_hook_service.execute_hooks_for(project, :destroy) + true + end + + private + + def remove_repository(path) + # Skip repository removal. We use this flag when remove user or group + return true if params[:skip_repo] == true + + # There is a possibility project does not have repository or wiki + return true unless gitlab_shell.exists?(path + '.git') + + new_path = removal_path(path) + + if gitlab_shell.mv_repository(path, new_path) + log_info("Repository \"#{path}\" moved to \"#{new_path}\"") + GitlabShellWorker.perform_in(5.minutes, :remove_repository, new_path) + else + false + end + end + + def raise_error(message) + raise DestroyError.new(message) + end + + # Build a path for removing repositories + # We use `+` because its not allowed by GitLab so user can not create + # project with name cookies+119+deleted and capture someone stalled repository + # + # gitlab/cookies.git -> gitlab/cookies+119+deleted.git + # + def removal_path(path) + "#{path}+#{project.id}#{DELETED_FLAG}" end end end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 4930660055a..50f208b11d1 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -1,57 +1,28 @@ module Projects class ForkService < BaseService - include Gitlab::ShellAdapter - def execute - @from_project = @project - - project_params = { - visibility_level: @from_project.visibility_level, - description: @from_project.description, + new_params = { + forked_from_project_id: @project.id, + visibility_level: @project.visibility_level, + description: @project.description, + name: @project.name, + path: @project.path, + namespace_id: @params[:namespace].try(:id) || current_user.namespace.id } - project = Project.new(project_params) - project.name = @from_project.name - project.path = @from_project.path - project.creator = @current_user - - if namespace = @params[:namespace] - project.namespace = namespace - else - project.namespace = @current_user.namespace + if @project.avatar.present? && @project.avatar.image? + new_params[:avatar] = @project.avatar end - unless @current_user.can?(:create_projects, project.namespace) - project.errors.add(:namespace, 'insufficient access rights') - return project - end + new_project = CreateService.new(current_user, new_params).execute - # If the project cannot save, we do not want to trigger the project destroy - # as this can have the side effect of deleting a repo attached to an existing - # project with the same name and namespace - if project.valid? - begin - Project.transaction do - #First save the DB entries as they can be rolled back if the repo fork fails - project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id) - if project.save - project.team << [@current_user, :master] - end - #Now fork the repo - unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path) - raise "forking failed in gitlab-shell" - end - project.ensure_satellite_exists - end - rescue => ex - project.errors.add(:base, "Fork transaction failed.") - project.destroy + if new_project.persisted? + if @project.gitlab_ci? + ForkRegistrationWorker.perform_async(@project.id, new_project.id, @current_user.private_token) end - else - project.errors.add(:base, "Invalid fork destination") end - project + new_project end end end diff --git a/app/services/projects/image_service.rb b/app/services/projects/image_service.rb deleted file mode 100644 index c79ddddd972..00000000000 --- a/app/services/projects/image_service.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Projects - class ImageService < BaseService - include Rails.application.routes.url_helpers - def initialize(repository, params, root_url) - @repository, @params, @root_url = repository, params.dup, root_url - end - - def execute - uploader = FileUploader.new('uploads', upload_path, accepted_images) - image = @params['markdown_img'] - - if image && correct_mime_type?(image) - alt = image.original_filename - uploader.store!(image) - link = { - 'alt' => File.basename(alt, '.*'), - 'url' => File.join(@root_url, uploader.url) - } - else - link = nil - end - end - - protected - - def upload_path - base_dir = FileUploader.generate_dir - File.join(@repository.path_with_namespace, base_dir) - end - - def accepted_images - %w(png jpg jpeg gif) - end - - def correct_mime_type?(image) - accepted_images.map{ |format| image.content_type.include? format }.any? - end - end -end diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index c4d2c0963b7..0004a399f47 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -1,43 +1,50 @@ module Projects class ParticipantsService < BaseService - def initialize(project) - @project = project - end - def execute(note_type, note_id) - participating = if note_type && note_id - participants_in(note_type, note_id) - else - [] - end - team_members = sorted(@project.team.members) - participants = all_members + team_members + participating + participating = + if note_type && note_id + participants_in(note_type, note_id) + else + [] + end + project_members = sorted(project.team.members) + participants = all_members + groups + project_members + participating participants.uniq end def participants_in(type, id) - users = case type - when "Issue" - issue = @project.issues.find_by_iid(id) - issue ? issue.participants : [] - when "MergeRequest" - merge_request = @project.merge_requests.find_by_iid(id) - merge_request ? merge_request.participants : [] - when "Commit" - author_ids = Note.for_commit_id(id).pluck(:author_id).uniq - User.where(id: author_ids) - else - [] - end + target = + case type + when "Issue" + project.issues.find_by_iid(id) + when "MergeRequest" + project.merge_requests.find_by_iid(id) + when "Commit" + project.commit(id) + end + + return [] unless target + + users = target.participants(current_user) sorted(users) end def sorted(users) - users.uniq.to_a.compact.sort_by(&:username).map { |user| { username: user.username, name: user.name } } + users.uniq.to_a.compact.sort_by(&:username).map do |user| + { username: user.username, name: user.name } + end + end + + def groups + current_user.authorized_groups.sort_by(&:path).map do |group| + count = group.users.count + { username: group.path, name: group.name, count: count } + end end def all_members - [{ username: "all", name: "Project and Group Members" }] + count = project.team.members.flatten.count + [{ username: "all", name: "All Project and Group Members", count: count }] end end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index e39fe882cb1..489e03bd5ef 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -12,7 +12,7 @@ module Projects class TransferError < StandardError; end def execute - namespace_id = params[:namespace_id] + namespace_id = params[:new_namespace_id] namespace = Namespace.find_by(id: namespace_id) if allowed_transfer?(current_user, project, namespace) @@ -43,6 +43,9 @@ module Projects project.namespace = new_namespace project.save! + # Notifications + project.send_move_instructions + # Move main repository unless gitlab_shell.mv_repository(old_path, new_path) raise TransferError.new('Cannot move project') diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 36877a61679..69bdd045ddf 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -2,8 +2,13 @@ module Projects class UpdateService < BaseService def execute # check that user is allowed to set specified visibility_level - unless can?(current_user, :change_visibility_level, project) && Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) - params[:visibility_level] = project.visibility_level + new_visibility = params[:visibility_level] + if new_visibility && new_visibility.to_i != project.visibility_level + unless can?(current_user, :change_visibility_level, project) && + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + deny_visibility_level(project, new_visibility) + return project + end end new_branch = params[:default_branch] diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb new file mode 100644 index 00000000000..992a7a7a1dc --- /dev/null +++ b/app/services/projects/upload_service.rb @@ -0,0 +1,28 @@ +module Projects + class UploadService < BaseService + def initialize(project, file) + @project, @file = project, file + end + + def execute + return nil unless @file and @file.size <= max_attachment_size + + uploader = FileUploader.new(@project) + uploader.store!(@file) + + filename = uploader.image? ? uploader.file.basename : uploader.file.filename + + { + 'alt' => filename, + 'url' => uploader.secure_url, + 'is_image' => uploader.image? + } + end + + private + + def max_attachment_size + current_application_settings.max_attachment_size.megabytes.to_i + end + end +end diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index 0bcc50c81a7..e904cb6c6fc 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -9,7 +9,7 @@ module Search def execute group = Group.find_by(id: params[:group_id]) if params[:group_id].present? projects = ProjectsFinder.new.execute(current_user) - projects = projects.where(namespace_id: group.id) if group + projects = projects.in_namespace(group.id) if group project_ids = projects.pluck(:id) Gitlab::SearchResults.new(project_ids, params[:search]) diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 44e494525b3..60235b6be2a 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -7,12 +7,12 @@ class SystemHooksService def execute_hooks(data) SystemHook.all.each do |sh| - async_execute_hook sh, data + async_execute_hook(sh, data, 'system_hooks') end end - def async_execute_hook(hook, data) - Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data) + def async_execute_hook(hook, data, hook_name) + Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data, hook_name) end def build_event_data(model, event) @@ -41,7 +41,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 : nil, + owner_email: owner.respond_to?(:email) ? owner.email : "", project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase }) when User @@ -60,6 +60,26 @@ class SystemHooksService access_level: model.human_access, project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase }) + when Group + owner = model.owner + + data.merge!( + name: model.name, + path: model.path, + group_id: model.id, + owner_name: owner.respond_to?(:name) ? owner.name : nil, + owner_email: owner.respond_to?(:email) ? owner.email : nil, + ) + when GroupMember + data.merge!( + group_name: model.group.name, + group_path: model.group.path, + group_id: model.group.id, + user_name: model.user.name, + user_email: model.user.email, + user_id: model.user.id, + group_access: model.human_access, + ) end end @@ -68,6 +88,9 @@ class SystemHooksService when ProjectMember return "user_add_to_team" if event == :create return "user_remove_from_team" if event == :destroy + when GroupMember + return 'user_add_to_group' if event == :create + return 'user_remove_from_group' if event == :destroy else "#{model.class.name.downcase}_#{event.to_s}" end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb new file mode 100644 index 00000000000..8253c1f780d --- /dev/null +++ b/app/services/system_note_service.rb @@ -0,0 +1,319 @@ +# SystemNoteService +# +# Used for creating system notes (e.g., when a user references a merge request +# from an issue, an issue's assignee changes, an issue is closed, etc.) +class SystemNoteService + # Called when commits are added to a Merge Request + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # new_commits - Array of Commits added since last push + # existing_commits - Array of Commits added in a previous push + # oldrev - Optional String SHA of a previous Commit + # + # See new_commit_summary and existing_commit_summary. + # + # Returns the created Note object + def self.add_commits(noteable, project, author, new_commits, existing_commits = [], oldrev = nil) + total_count = new_commits.length + existing_commits.length + commits_text = "#{total_count} commit".pluralize(total_count) + + body = "Added #{commits_text}:\n\n" + body << existing_commit_summary(noteable, existing_commits, oldrev) + body << new_commit_summary(new_commits).join("\n") + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the assignee of a Noteable is changed or removed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # assignee - User being assigned, or nil + # + # Example Note text: + # + # "Assignee removed" + # + # "Reassigned to @rspeicher" + # + # Returns the created Note object + def self.change_assignee(noteable, project, author, assignee) + body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when one or more labels on a Noteable are added and/or removed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # added_labels - Array of Labels added + # removed_labels - Array of Labels removed + # + # Example Note text: + # + # "Added ~1 and removed ~2 ~3 labels" + # + # "Added ~4 label" + # + # "Removed ~5 label" + # + # Returns the created Note object + def self.change_label(noteable, project, author, added_labels, removed_labels) + labels_count = added_labels.count + removed_labels.count + + references = ->(label) { "~#{label.id}" } + added_labels = added_labels.map(&references).join(' ') + removed_labels = removed_labels.map(&references).join(' ') + + body = '' + + if added_labels.present? + body << "added #{added_labels}" + body << ' and ' if removed_labels.present? + end + + if removed_labels.present? + body << "removed #{removed_labels}" + end + + body << ' ' << 'label'.pluralize(labels_count) + body = "#{body.capitalize}" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the milestone of a Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # milestone - Milestone being assigned, or nil + # + # Example Note text: + # + # "Milestone removed" + # + # "Miletone changed to 7.11" + # + # Returns the created Note object + def self.change_milestone(noteable, project, author, milestone) + body = 'Milestone ' + body += milestone.nil? ? 'removed' : "changed to #{milestone.title}" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the status of a Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # status - String status + # source - Mentionable performing the change, or nil + # + # Example Note text: + # + # "Status changed to merged" + # + # "Status changed to closed by bc17db76" + # + # Returns the created Note object + def self.change_status(noteable, project, author, status, source) + body = "Status changed to #{status}" + body += " by #{source.gfm_reference}" if source + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the title of a Noteable is changed + # + # noteable - Noteable object that responds to `title` + # project - Project owning noteable + # author - User performing the change + # old_title - Previous String title + # + # Example Note text: + # + # "Title changed from **Old** to **New**" + # + # Returns the created Note object + def self.change_title(noteable, project, author, old_title) + return unless noteable.respond_to?(:title) + + body = "Title changed from **#{old_title}** to **#{noteable.title}**" + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when a branch in Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # branch_type - 'source' or 'target' + # old_branch - old branch name + # new_branch - new branch nmae + # + # Example Note text: + # + # "Target branch changed from `Old` to `New`" + # + # Returns the created Note object + def self.change_branch(noteable, project, author, branch_type, old_branch, new_branch) + body = "#{branch_type} branch changed from `#{old_branch}` to `#{new_branch}`".capitalize + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when a Mentionable references a Noteable + # + # noteable - Noteable object being referenced + # mentioner - Mentionable object + # author - User performing the reference + # + # Example Note text: + # + # "mentioned in #1" + # + # "mentioned in !2" + # + # "mentioned in 54f7727c" + # + # See cross_reference_note_content. + # + # Returns the created Note object + def self.cross_reference(noteable, mentioner, author) + return if cross_reference_disallowed?(noteable, mentioner) + + gfm_reference = mentioner.gfm_reference(noteable.project) + + note_options = { + project: noteable.project, + author: author, + note: cross_reference_note_content(gfm_reference) + } + + if noteable.kind_of?(Commit) + note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id) + else + note_options.merge!(noteable: noteable) + end + + create_note(note_options) + end + + def self.cross_reference?(note_text) + note_text.start_with?(cross_reference_note_prefix) + end + + # Check if a cross-reference is disallowed + # + # This method prevents adding a "mentioned in !1" note on every single commit + # in a merge request. Additionally, it prevents the creation of references to + # external issues (which would fail). + # + # noteable - Noteable object being referenced + # mentioner - Mentionable object + # + # Returns Boolean + def self.cross_reference_disallowed?(noteable, mentioner) + return true if noteable.is_a?(ExternalIssue) + return false unless mentioner.is_a?(MergeRequest) + return false unless noteable.is_a?(Commit) + + mentioner.commits.include?(noteable) + end + + # Check if a cross reference to a noteable from a mentioner already exists + # + # This method is used to prevent multiple notes being created for a mention + # when a issue is updated, for example. + # + # noteable - Noteable object being referenced + # mentioner - Mentionable object + # + # Returns Boolean + def self.cross_reference_exists?(noteable, mentioner) + # Initial scope should be system notes of this noteable type + notes = Note.system.where(noteable_type: noteable.class) + + if noteable.is_a?(Commit) + # Commits have non-integer IDs, so they're stored in `commit_id` + notes = notes.where(commit_id: noteable.id) + else + notes = notes.where(noteable_id: noteable.id) + end + + gfm_reference = mentioner.gfm_reference(noteable.project) + notes = notes.where(note: cross_reference_note_content(gfm_reference)) + + notes.count > 0 + end + + private + + def self.create_note(args = {}) + Note.create(args.merge(system: true)) + end + + def self.cross_reference_note_prefix + 'mentioned in ' + end + + def self.cross_reference_note_content(gfm_reference) + "#{cross_reference_note_prefix}#{gfm_reference}" + end + + # Build an Array of lines detailing each commit added in a merge request + # + # new_commits - Array of new Commit objects + # + # Returns an Array of Strings + def self.new_commit_summary(new_commits) + new_commits.collect do |commit| + "* #{commit.short_id} - #{commit.title}" + end + end + + # Build a single line summarizing existing commits being added in a merge + # request + # + # noteable - MergeRequest object + # existing_commits - Array of existing Commit objects + # oldrev - Optional String SHA of a previous Commit + # + # Examples: + # + # "* ea0f8418...2f4426b7 - 24 commits from branch `master`" + # + # "* ea0f8418..4188f0ea - 15 commits from branch `fork:master`" + # + # "* ea0f8418 - 1 commit from branch `feature`" + # + # Returns a newline-terminated String + def self.existing_commit_summary(noteable, existing_commits, oldrev = nil) + return '' if existing_commits.empty? + + count = existing_commits.size + + commit_ids = if count == 1 + existing_commits.first.short_id + else + if oldrev + "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}" + else + "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}" + end + end + + commits_text = "#{count} commit".pluralize(count) + + branch = noteable.target_branch + branch = "#{noteable.target_project_namespace}:#{branch}" if noteable.for_fork? + + "* #{commit_ids} - #{commits_text} from branch `#{branch}`\n" + end +end diff --git a/app/services/test_hook_service.rb b/app/services/test_hook_service.rb index b6b1ef29b51..e85e58751e7 100644 --- a/app/services/test_hook_service.rb +++ b/app/services/test_hook_service.rb @@ -1,9 +1,6 @@ class TestHookService def execute(hook, current_user) - data = GitPushService.new.sample_data(hook.project, current_user) - hook.execute(data) - true - rescue SocketError - false + data = Gitlab::PushDataBuilder.build_sample(hook.project, current_user) + hook.execute(data, 'push_hooks') end end diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb new file mode 100644 index 00000000000..9d181c2d2ab --- /dev/null +++ b/app/services/update_snippet_service.rb @@ -0,0 +1,22 @@ +class UpdateSnippetService < BaseService + attr_accessor :snippet + + def initialize(project, user, snippet, params) + super(project, user, params) + @snippet = snippet + end + + def execute + # check that user is allowed to set specified visibility_level + new_visibility = params[:visibility_level] + if new_visibility && new_visibility.to_i != snippet.visibility_level + unless can?(current_user, :change_visibility_level, snippet) && + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + deny_visibility_level(snippet, new_visibility) + return snippet + end + end + + snippet.update_attributes(params) + end +end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index 29a55b36ca5..a9691bee46e 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -3,8 +3,6 @@ class AttachmentUploader < CarrierWave::Uploader::Base storage :file - after :store, :reset_events_cache - def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end @@ -22,19 +20,7 @@ class AttachmentUploader < CarrierWave::Uploader::Base false end - def secure_url - Gitlab.config.gitlab.relative_url_root + "/files/#{model.class.to_s.underscore}/#{model.id}/#{file.filename}" - end - - def url - Gitlab.config.gitlab.relative_url_root + super unless super.nil? - end - def file_storage? self.class.storage == CarrierWave::Storage::File end - - def reset_events_cache(file) - model.reset_events_cache if model.is_a?(User) - end end diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb new file mode 100644 index 00000000000..7cad044555b --- /dev/null +++ b/app/uploaders/avatar_uploader.rb @@ -0,0 +1,32 @@ +# encoding: utf-8 + +class AvatarUploader < CarrierWave::Uploader::Base + storage :file + + after :store, :reset_events_cache + + def store_dir + "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + end + + def image? + img_ext = %w(png jpg jpeg gif bmp tiff) + if file.respond_to?(:extension) + img_ext.include?(file.extension.downcase) + else + # Not all CarrierWave storages respond to :extension + ext = file.path.split('.').last.downcase + img_ext.include?(ext) + end + rescue + false + end + + def file_storage? + self.class.storage == CarrierWave::Storage::File + end + + def reset_events_cache(file) + model.reset_events_cache if model.is_a?(User) + end +end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 0fa987c93f6..f9673abbfe8 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -2,40 +2,47 @@ class FileUploader < CarrierWave::Uploader::Base storage :file - def initialize(base_dir, path = '', allowed_extensions = nil) - @base_dir = base_dir - @path = path - @allowed_extensions = allowed_extensions + attr_accessor :project, :secret + + def initialize(project, secret = self.class.generate_secret) + @project = project + @secret = secret end def base_dir - @base_dir + "uploads" end def store_dir - File.join(@base_dir, @path) + File.join(base_dir, @project.path_with_namespace, @secret) end def cache_dir - File.join(@base_dir, 'tmp', @path) + File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) end - def extension_white_list - @allowed_extensions + def self.generate_secret + SecureRandom.hex end - def store!(file) - @filename = self.class.generate_filename(file) - super + def secure_url + File.join(Gitlab.config.gitlab.url, @project.path_with_namespace, "uploads", @secret, file.filename) end - def self.generate_filename(file) - original_filename = File.basename(file.original_filename, '.*') - extension = File.extname(file.original_filename) - new_filename = Digest::MD5.hexdigest(original_filename) + extension + def file_storage? + self.class.storage == CarrierWave::Storage::File end - def self.generate_dir - SecureRandom.hex(5) + def image? + img_ext = %w(png jpg jpeg gif bmp tiff) + if file.respond_to?(:extension) + img_ext.include?(file.extension.downcase) + else + # Not all CarrierWave storages respond to :extension + ext = file.path.split('.').last.downcase + img_ext.include?(ext) + end + rescue + false end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml new file mode 100644 index 00000000000..d5a49fc41f4 --- /dev/null +++ b/app/views/admin/application_settings/_form.html.haml @@ -0,0 +1,105 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + - if @application_setting.errors.any? + #error_explanation + .alert.alert-danger + - @application_setting.errors.full_messages.each do |msg| + %p= msg + + %fieldset + %legend Features + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :signup_enabled do + = f.check_box :signup_enabled + Signup enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :signin_enabled do + = f.check_box :signin_enabled + Signin enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :gravatar_enabled do + = f.check_box :gravatar_enabled + Gravatar enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :twitter_sharing_enabled do + = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block' + Twitter enabled + %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :version_check_enabled do + = f.check_box :version_check_enabled + Version check enabled + %fieldset + %legend Misc + .form-group + = f.label :default_projects_limit, class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :default_projects_limit, class: 'form-control' + .form-group + = f.label :default_branch_protection, class: 'control-label col-sm-2' + .col-sm-10 + = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' + .form-group.project-visibility-level-holder + = f.label :default_project_visibility, class: 'control-label col-sm-2' + .col-sm-10 + = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: 'Project') + .form-group.project-visibility-level-holder + = f.label :default_snippet_visibility, class: 'control-label col-sm-2' + .col-sm-10 + = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: 'Snippet') + .form-group + = f.label :restricted_visibility_levels, class: 'control-label col-sm-2' + .col-sm-10 + - data_attrs = { toggle: 'buttons' } + .btn-group{ data: data_attrs } + - restricted_level_checkboxes('restricted-visibility-help').each do |level| + = level + %span.help-block#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets + .form-group + = f.label :home_page_url, class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' + %span.help-block#home_help_block We will redirect non-logged in users to this page + .form-group + = f.label :after_sign_out_path, class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block' + %span.help-block#after_sign_out_path_help_block We will redirect users to this page after they sign out + .form-group + = f.label :sign_in_text, class: 'control-label col-sm-2' + .col-sm-10 + = f.text_area :sign_in_text, class: 'form-control', rows: 4 + .help-block Markdown enabled + .form-group + = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :max_attachment_size, class: 'form-control' + .form-group + = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :session_expire_delay, class: 'form-control' + %span.help-block#session_expire_delay_help_block GitLab restart is required to apply changes + .form-group + = f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control' + .help-block Only users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com + .form_group + = f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2' + .col-sm-10 + .checkbox + = f.label :user_oauth_applications do + = f.check_box :user_oauth_applications + Allow users to register any application to use GitLab as an OAuth provider + + .form-actions + = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml new file mode 100644 index 00000000000..1632dd8affa --- /dev/null +++ b/app/views/admin/application_settings/show.html.haml @@ -0,0 +1,4 @@ +- page_title "Settings" +%h3.page-title Application settings +%hr += render 'form' diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml new file mode 100644 index 00000000000..3147cbd659f --- /dev/null +++ b/app/views/admin/applications/_delete_form.html.haml @@ -0,0 +1,4 @@ +- submit_btn_css ||= 'btn btn-link btn-remove btn-sm' += form_tag admin_application_path(application) do + %input{:name => "_method", :type => "hidden", :value => "delete"}/ + = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
\ No newline at end of file diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml new file mode 100644 index 00000000000..fa4e6335c73 --- /dev/null +++ b/app/views/admin/applications/_form.html.haml @@ -0,0 +1,26 @@ += form_for [:admin, @application], url: @url, html: {class: 'form-horizontal', role: 'form'} do |f| + - if application.errors.any? + .alert.alert-danger + %button{ type: "button", class: "close", "data-dismiss" => "alert"} × + - application.errors.full_messages.each do |msg| + %p= msg + = content_tag :div, class: 'form-group' do + = f.label :name, class: 'col-sm-2 control-label' + .col-sm-10 + = f.text_field :name, class: 'form-control' + = doorkeeper_errors_for application, :name + = content_tag :div, class: 'form-group' do + = f.label :redirect_uri, class: 'col-sm-2 control-label' + .col-sm-10 + = f.text_area :redirect_uri, class: 'form-control' + = doorkeeper_errors_for application, :redirect_uri + %span.help-block + Use one line per URI + - if Doorkeeper.configuration.native_redirect_uri + %span.help-block + Use + %code= Doorkeeper.configuration.native_redirect_uri + for local tests + .form-actions + = f.submit 'Submit', class: "btn btn-primary wide" + = link_to "Cancel", admin_applications_path, class: "btn btn-default" diff --git a/app/views/admin/applications/edit.html.haml b/app/views/admin/applications/edit.html.haml new file mode 100644 index 00000000000..c596866bde2 --- /dev/null +++ b/app/views/admin/applications/edit.html.haml @@ -0,0 +1,4 @@ +- page_title "Edit", @application.name, "Applications" +%h3.page-title Edit application +- @url = admin_application_path(@application) += render 'form', application: @application diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml new file mode 100644 index 00000000000..fc921a966f3 --- /dev/null +++ b/app/views/admin/applications/index.html.haml @@ -0,0 +1,23 @@ +- page_title "Applications" +%h3.page-title + System OAuth applications +%p.light + System OAuth application does not belong to certain user and can be managed only by admins +%hr +%p= link_to 'New Application', new_admin_application_path, class: 'btn btn-success' +%table.table.table-striped + %thead + %tr + %th Name + %th Callback URL + %th Clients + %th + %th + %tbody.oauth-applications + - @applications.each do |application| + %tr{:id => "application_#{application.id}"} + %td= link_to application.name, admin_application_path(application) + %td= application.redirect_uri + %td= application.access_tokens.map(&:resource_owner_id).uniq.count + %td= link_to 'Edit', edit_admin_application_path(application), class: 'btn btn-link' + %td= render 'delete_form', application: application diff --git a/app/views/admin/applications/new.html.haml b/app/views/admin/applications/new.html.haml new file mode 100644 index 00000000000..6310d89bd6b --- /dev/null +++ b/app/views/admin/applications/new.html.haml @@ -0,0 +1,4 @@ +- page_title "New Application" +%h3.page-title New application +- @url = admin_applications_path += render 'form', application: @application diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml new file mode 100644 index 00000000000..0ea2ffeda99 --- /dev/null +++ b/app/views/admin/applications/show.html.haml @@ -0,0 +1,27 @@ +- page_title @application.name, "Applications" +%h3.page-title + Application: #{@application.name} + + +%table.table + %tr + %td + Application Id + %td + %code#application_id= @application.uid + %tr + %td + Secret: + %td + %code#secret= @application.secret + + %tr + %td + Callback url + %td + - @application.redirect_uri.split.each do |uri| + %div + %span.monospace= uri +.form-actions + = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left' + = render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10' diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 8db2b2a709c..3a01e115109 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -1,3 +1,4 @@ +- page_title "Background Jobs" %h3.page-title Background Jobs %p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing @@ -41,4 +42,4 @@ .panel.panel-default - %iframe{src: sidekiq_path, width: '100%', height: 900, style: "border: none"} + %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"} diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index 7b483ee6556..17dffebd360 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Broadcast Messages" %h3.page-title Broadcast Messages %p.light @@ -21,13 +22,11 @@ .form-group.js-toggle-colors-container.hide = f.label :color, "Background Color", class: 'control-label' .col-sm-10 - = f.text_field :color, placeholder: "#AA33EE", class: "form-control" - .light 6 character hex values starting with a # sign. + = f.color_field :color, value: "#eb9532", class: "form-control" .form-group.js-toggle-colors-container.hide = f.label :font, "Font Color", class: 'control-label' .col-sm-10 - = f.text_field :font, placeholder: "#224466", class: "form-control" - .light 6 character hex values starting with a # sign. + = f.color_field :font, value: "#FFFFFF", class: "form-control" .form-group = f.label :starts_at, class: 'control-label' .col-sm-10.datetime-controls @@ -52,7 +51,7 @@ %strong #{broadcast_message.ends_at.to_s(:short)} - = link_to [:admin, broadcast_message], method: :delete, remote: true, class: 'remove-row btn btn-tiny' do + = link_to [:admin, broadcast_message], method: :delete, remote: true, class: 'remove-row btn btn-xs' do %i.fa.fa-times.cred .message= broadcast_message.message diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 7427cea7e8b..3732ff847b9 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -1,69 +1,7 @@ -%h3.page-title - Admin area -%p.light - You can manage projects, users and other GitLab data from here. -%hr .admin-dashboard .row - .col-sm-4 - .light-well - %h4 Projects - .data - = link_to admin_projects_path do - %h1= Project.count - %hr - = link_to 'New Project', new_project_path, class: "btn btn-new" - .col-sm-4 - .light-well - %h4 Users - .data - = link_to admin_users_path do - %h1= User.count - %hr - = link_to 'New User', new_admin_user_path, class: "btn btn-new" - .col-sm-4 - .light-well - %h4 Groups - .data - = link_to admin_groups_path do - %h1= Group.count - %hr - = link_to 'New Group', new_admin_group_path, class: "btn btn-new" - - .row.prepend-top-10 - .col-md-4 - %h4 Latest projects - %hr - - @projects.each do |project| - %p - = link_to project.name_with_namespace, [:admin, project], class: 'str-truncated' - %span.light.pull-right - #{time_ago_with_tooltip(project.created_at)} - .col-md-4 - %h4 Latest users - %hr - - @users.each do |user| - %p - = link_to [:admin, user], class: 'str-truncated' do - = user.name - %span.light.pull-right - #{time_ago_with_tooltip(user.created_at)} - - .col-md-4 - %h4 Latest groups - %hr - - @groups.each do |group| - %p - = link_to [:admin, group], class: 'str-truncated' do - = group.name - %span.light.pull-right - #{time_ago_with_tooltip(group.created_at)} - - %br - .row - .col-md-4 - %h4 Stats + %h4 Statistics %hr %p Forks @@ -94,9 +32,9 @@ %span.light.pull-right = Milestone.count %p - Active users last 30 days + Active Users %span.light.pull-right - = User.where("current_sign_in_at > ?", 30.days.ago).count + = User.active.count .col-md-4 %h4 Features @@ -104,7 +42,7 @@ %p Sign up %span.light.pull-right - = boolean_to_icon gitlab_config.signup_enabled + = boolean_to_icon signup_enabled? %p LDAP %span.light.pull-right @@ -112,13 +50,18 @@ %p Gravatar %span.light.pull-right - = boolean_to_icon Gitlab.config.gravatar.enabled + = boolean_to_icon gravatar_enabled? %p OmniAuth %span.light.pull-right = boolean_to_icon Gitlab.config.omniauth.enabled .col-md-4 - %h4 Components + %h4 + Components + - if current_application_settings.version_check_enabled + .pull-right + = version_status_badge + %hr %p GitLab @@ -141,3 +84,59 @@ Rails %span.pull-right #{Rails::VERSION::STRING} + %hr + .row + .col-sm-4 + .light-well + %h4 Projects + .data + = link_to admin_namespaces_projects_path do + %h1= Project.count + %hr + = link_to('New Project', new_project_path, class: "btn btn-new") + .col-sm-4 + .light-well + %h4 Users + .data + = link_to admin_users_path do + %h1= User.count + %hr + = link_to 'New User', new_admin_user_path, class: "btn btn-new" + .col-sm-4 + .light-well + %h4 Groups + .data + = link_to admin_groups_path do + %h1= Group.count + %hr + = link_to 'New Group', new_admin_group_path, class: "btn btn-new" + + .row.prepend-top-10 + .col-md-4 + %h4 Latest projects + %hr + - @projects.each do |project| + %p + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated' + %span.light.pull-right + #{time_ago_with_tooltip(project.created_at)} + + .col-md-4 + %h4 Latest users + %hr + - @users.each do |user| + %p + = link_to [:admin, user], class: 'str-truncated' do + = user.name + %span.light.pull-right + #{time_ago_with_tooltip(user.created_at)} + + .col-md-4 + %h4 Latest groups + %hr + - @groups.each do |group| + %p + = link_to [:admin, group], class: 'str-truncated' do + = group.name + %span.light.pull-right + #{time_ago_with_tooltip(group.created_at)} diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml new file mode 100644 index 00000000000..6405a69fad3 --- /dev/null +++ b/app/views/admin/deploy_keys/index.html.haml @@ -0,0 +1,27 @@ +- page_title "Deploy Keys" +.panel.panel-default + .panel-heading + Public deploy keys (#{@deploy_keys.count}) + .panel-head-actions + = link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm" + - if @deploy_keys.any? + %table.table + %thead.panel-heading + %tr + %th Title + %th Fingerprint + %th Added at + %th + %tbody + - @deploy_keys.each do |deploy_key| + %tr + %td + = link_to admin_deploy_key_path(deploy_key) do + %strong= deploy_key.title + %td + %code.key-fingerprint= deploy_key.fingerprint + %td + %span.cgray + added #{time_ago_with_tooltip(deploy_key.created_at)} + %td + = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right" diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml new file mode 100644 index 00000000000..5b46b3222a9 --- /dev/null +++ b/app/views/admin/deploy_keys/new.html.haml @@ -0,0 +1,27 @@ +- page_title "New Deploy Key" +%h3.page-title New public deploy key +%hr + +%div + = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f| + -if @deploy_key.errors.any? + .alert.alert-danger + %ul + - @deploy_key.errors.full_messages.each do |msg| + %li= msg + + .form-group + = f.label :title, class: "control-label" + .col-sm-10= f.text_field :title, class: 'form-control' + .form-group + = f.label :key, class: "control-label" + .col-sm-10 + %p.light + Paste a machine public key here. Read more about how to generate it + = link_to "here", help_page_path("ssh", "README") + = f.text_area :key, class: "form-control thin_area", rows: 5 + + .form-actions + = f.submit 'Create', class: "btn-create btn" + = link_to "Cancel", admin_deploy_keys_path, class: "btn btn-cancel" + diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index f4d7e25fd74..8de2ba74a79 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -12,26 +12,14 @@ - if @group.new_record? .form-group - .col-sm-2 - .col-sm-10 - .bs-callout.bs-callout-info + .col-sm-offset-2.col-sm-10 + .alert.alert-info = render 'shared/group_tips' .form-actions = f.submit 'Create group', class: "btn btn-create" = link_to 'Cancel', admin_groups_path, class: "btn btn-cancel" - else - .form-group.group_name_holder - = f.label :path, class: 'control-label' do - %span Group path - .col-sm-10 - = f.text_field :path, placeholder: "example-group", class: "form-control danger" - .bs-callout.bs-callout-danger - %ul - %li Changing group path can have unintended side effects. - %li Renaming group path will rename directory for all related projects - %li It will change web url for access group and group projects. - %li It will change the git path to repositories under this group. .form-actions = f.submit 'Save changes', class: "btn btn-primary" = link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel" diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml index 824e51c1cf1..eb09a6328ed 100644 --- a/app/views/admin/groups/edit.html.haml +++ b/app/views/admin/groups/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "Edit", @group.name, "Groups" %h3.page-title Edit group: #{@group.name} %hr = render 'form' diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 1d7fef43184..5ce7cdf2f8d 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Groups" %h3.page-title Groups (#{@groups.total_count}) = link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right" @@ -8,10 +9,31 @@ %hr = form_tag admin_groups_path, method: :get, class: 'form-inline' do + = hidden_field_tag :sort, @sort .form-group - = text_field_tag :name, params[:name], class: "form-control input-mn-300" + = text_field_tag :name, params[:name], class: "form-control" = button_tag "Search", class: "btn submit btn-primary" + .pull-right + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %span.light sort: + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_recently_created + %b.caret + %ul.dropdown-menu + %li + = link_to admin_groups_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to admin_groups_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to admin_groups_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to admin_groups_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated + %hr %ul.bordered-list @@ -19,8 +41,8 @@ %li .clearfix .pull-right.prepend-top-10 - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-small" - = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-small btn-remove" + = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-sm" + = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-sm btn-remove" %h4 = link_to [:admin, group] do diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml index f46f45c5514..c81ee552ac3 100644 --- a/app/views/admin/groups/new.html.haml +++ b/app/views/admin/groups/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Group" %h3.page-title New group %hr = render 'form' diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 8057de38805..187314872de 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -1,3 +1,4 @@ +- page_title @group.name, "Groups" %h3.page-title Group: #{@group.name} @@ -12,7 +13,7 @@ Group info: %ul.well-list %li - = image_tag group_icon(@group.path), class: "avatar s60" + = image_tag group_icon(@group), class: "avatar s60" %li %span.light Name: %strong= @group.name @@ -41,7 +42,7 @@ - @projects.each do |project| %li %strong - = link_to project.name_with_namespace, [:admin, project] + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] %span.label.label-gray = repository_size(project) %span.pull-right.light @@ -58,13 +59,13 @@ Read more about project permissions %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" - = form_tag project_teams_update_admin_group_path(@group), id: "new_team_member", class: "bulk_import", method: :put do + = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do %div - = users_select_tag(:user_ids, multiple: true) + = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all) %div.prepend-top-10 = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" %hr - = button_tag 'Add users into group', class: "btn btn-create" + = button_tag 'Add users to group', class: "btn btn-create" .panel.panel-default .panel-heading %h3.panel-title @@ -74,13 +75,18 @@ %ul.well-list.group-users-list - @members.each do |member| - user = member.user - %li{class: dom_class(member), id: dom_id(user)} + %li{class: dom_class(member), id: (dom_id(user) if user)} .list-item-name - %strong - = link_to user.name, admin_user_path(user) + - if user + %strong + = link_to user.name, admin_user_path(user) + - else + %strong + = member.invite_email + (invited) %span.pull-right.light = member.human_access - = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do + = 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 .panel-footer = paginate @members, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index 0c5db0805f9..e74e1e85f41 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -1,3 +1,4 @@ +- page_title "System Hooks" %h3.page-title System hooks @@ -33,5 +34,5 @@ %strong= hook.url .pull-right - = link_to 'Test Hook', admin_hook_test_path(hook), class: "btn btn-small" - = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-small" + = link_to 'Test Hook', admin_hook_test_path(hook), class: "btn btn-sm" + = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" diff --git a/app/views/admin/keys/show.html.haml b/app/views/admin/keys/show.html.haml new file mode 100644 index 00000000000..9ee77c77398 --- /dev/null +++ b/app/views/admin/keys/show.html.haml @@ -0,0 +1,2 @@ +- page_title @key.title, "Keys" += render "profiles/keys/key_details", admin: true diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 384c6ee9af5..1484baa78e0 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,3 +1,4 @@ +- page_title "Logs" - loggers = [Gitlab::GitLogger, Gitlab::AppLogger, Gitlab::ProductionLogger, Gitlab::SidekiqLogger] %ul.nav.nav-tabs.log-tabs diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 2cd6b12be7f..f43d46356fa 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,7 +1,10 @@ +- page_title "Projects" += render 'shared/show_aside' + .row - .col-md-3 + %aside.col-md-3 .admin-filter - = form_tag admin_projects_path, method: :get, class: '' do + = form_tag admin_namespaces_projects_path, method: :get, class: '' do .form-group = label_tag :name, 'Name:' = text_field_tag :name, params[:name], class: "form-control" @@ -13,15 +16,13 @@ .form-group %strong Activity .checkbox - = label_tag :with_push, 'Not empty' - = check_box_tag :with_push, 1, params[:with_push] - - %span.light Projects with push events + = label_tag :with_push do + = check_box_tag :with_push, 1, params[:with_push] + %span Projects with push events .checkbox - = label_tag :abandoned, 'Abandoned' - = check_box_tag :abandoned, 1, params[:abandoned] - - %span.light No activity over 6 month + = label_tag :abandoned do + = check_box_tag :abandoned, 1, params[:abandoned] + %span No activity over 6 month %fieldset %strong Visibility level: @@ -36,48 +37,46 @@ %hr = hidden_field_tag :sort, params[:sort] = button_tag "Search", class: "btn submit btn-primary" - = link_to "Reset", admin_projects_path, class: "btn btn-cancel" + = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" - .col-md-9 + %section.col-md-9 .panel.panel-default .panel-heading Projects (#{@projects.total_count}) .panel-head-actions .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} %span.light sort: - if @sort.present? - = @sort.humanize + = sort_options_hash[@sort] - else - Name + = sort_title_recently_created %b.caret %ul.dropdown-menu %li - = link_to admin_projects_path(sort: nil) do - Name - = link_to admin_projects_path(sort: 'newest') do - Newest - = link_to admin_projects_path(sort: 'oldest') do - Oldest - = link_to admin_projects_path(sort: 'recently_updated') do - Recently updated - = link_to admin_projects_path(sort: 'last_updated') do - Last updated - = link_to admin_projects_path(sort: 'largest_repository') do - Largest repository - = link_to 'New Project', new_project_path, class: "btn btn-new" + = link_to admin_namespaces_projects_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to admin_namespaces_projects_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to admin_namespaces_projects_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to admin_namespaces_projects_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated + = link_to admin_namespaces_projects_path(sort: sort_value_largest_repo) do + = sort_title_largest_repo + = link_to 'New Project', new_project_path, class: "btn btn-sm btn-success" %ul.well-list - @projects.each do |project| %li .list-item-name %span{ class: visibility_level_color(project.visibility_level) } = visibility_level_icon(project.visibility_level) - = link_to project.name_with_namespace, [:admin, project] + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] .pull-right %span.label.label-gray = repository_size(project) - = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" - = link_to 'Destroy', [project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-small btn-remove" + = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" + = link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-sm btn-remove" - if @projects.blank? .nothing-here-block 0 projects matches = paginate @projects, theme: "gitlab" diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 6d536199851..5260eadf95b 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -1,3 +1,4 @@ +- page_title @project.name_with_namespace, "Projects" %h3.page-title Project: #{@project.name_with_namespace} = link_to edit_project_path(@project), class: "btn pull-right" do @@ -42,11 +43,11 @@ %li %span.light http: %strong - = link_to @project.http_url_to_repo + = link_to @project.http_url_to_repo, project_path(@project) %li %span.light ssh: %strong - = link_to @project.ssh_url_to_repo + = link_to @project.ssh_url_to_repo, project_path(@project) - if @project.repository.exists? %li %span.light fs: @@ -68,6 +69,11 @@ %strong.cred does not exist + - if @project.archived? + %li + %span.light archived: + %strong repository is read-only + %li %span.light access: %strong @@ -79,15 +85,14 @@ .panel-heading Transfer project .panel-body - = form_for @project, url: transfer_admin_project_path(@project), method: :put, html: { class: 'form-horizontal' } do |f| + = form_for @project, url: transfer_admin_namespace_project_path(@project.namespace, @project), method: :put, html: { class: 'form-horizontal' } do |f| .form-group - = f.label :namespace_id, "Namespace", class: 'control-label' + = f.label :new_namespace_id, "Namespace", class: 'control-label' .col-sm-10 - = namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large' + = namespace_select_tag :new_namespace_id, selected: params[:namespace_id], class: 'input-large' .form-group - .col-sm-2 - .col-sm-10 + .col-sm-offset-2.col-sm-10 = f.submit 'Transfer', class: 'btn btn-primary' .col-md-6 @@ -97,7 +102,7 @@ %strong #{@group.name} group members (#{@group.group_members.count}) .pull-right - = link_to admin_group_path(@group), class: 'btn btn-small' do + = link_to admin_group_path(@group), class: 'btn btn-xs' do %i.fa.fa-pencil-square-o %ul.well-list - @group_members.each do |member| @@ -111,22 +116,27 @@ %small (#{@project.users.count}) .pull-right - = link_to project_team_index_path(@project), class: "btn btn-tiny" do + = link_to namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-xs" do %i.fa.fa-pencil-square-o Manage Access - %ul.well-list.team_members + %ul.well-list.project_members - @project_members.each do |project_member| - user = project_member.user %li.project_member .list-item-name - %strong - = link_to user.name, admin_user_path(user) + - if user + %strong + = link_to user.name, admin_user_path(user) + - else + %strong + = project_member.invite_email + (invited) .pull-right - if project_member.owner? %span.light Owner - else %span.light= project_member.human_access - = link_to project_team_member_path(@project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-small btn-remove" do + = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_from_project_team_message(@project, project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do %i.fa.fa-times .panel-footer = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml new file mode 100644 index 00000000000..cdbfc60f9a4 --- /dev/null +++ b/app/views/admin/services/_form.html.haml @@ -0,0 +1,10 @@ +%h3.page-title + = @service.title + +%p #{@service.description} template + += form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'form-horizontal fieldset-form' } do |form| + = render 'shared/service_settings', form: form + + .form-actions + = form.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/services/edit.html.haml b/app/views/admin/services/edit.html.haml new file mode 100644 index 00000000000..53d970e33c1 --- /dev/null +++ b/app/views/admin/services/edit.html.haml @@ -0,0 +1,2 @@ +- page_title @service.title, "Service Templates" += render 'form' diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml new file mode 100644 index 00000000000..e2377291142 --- /dev/null +++ b/app/views/admin/services/index.html.haml @@ -0,0 +1,23 @@ +- page_title "Service Templates" +%h3.page-title Service templates +%p.light Service template allows you to set default values for project services + +%table.table + %thead + %tr + %th + %th Service + %th Description + %th Last edit + - @services.sort_by(&:title).each do |service| + %tr + %td + = icon("copy", class: 'clgray') + %td + = link_to edit_admin_application_settings_service_path(service.id) do + %strong= service.title + %td + = service.description + %td.light + = time_ago_in_words service.updated_at + ago diff --git a/app/views/admin/users/edit.html.haml b/app/views/admin/users/edit.html.haml index d71d8189c51..a8837d74dd9 100644 --- a/app/views/admin/users/edit.html.haml +++ b/app/views/admin/users/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "Edit", @user.name, "Users" %h3.page-title Edit user: #{@user.name} .back-link diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 92c619738a2..45dee86b017 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,5 +1,8 @@ +- page_title "Users" += render 'shared/show_aside' + .row - .col-md-3 + %aside.col-md-3 .admin-filter %ul.nav.nav-pills.nav-stacked %li{class: "#{'active' unless params[:filter]}"} @@ -27,32 +30,37 @@ %hr = link_to 'Reset', admin_users_path, class: "btn btn-cancel" - .col-md-9 + %section.col-md-9 .panel.panel-default .panel-heading Users (#{@users.total_count}) .panel-head-actions .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} %span.light sort: - if @sort.present? - = @sort.humanize + = sort_options_hash[@sort] - else - Name + = sort_title_name %b.caret %ul.dropdown-menu %li - = link_to admin_users_path(sort: nil) do - Name - = link_to admin_users_path(sort: 'recent_sign_in') do - Recent sign in - = link_to admin_users_path(sort: 'oldest_sign_in') do - Oldest sign in - = link_to admin_users_path(sort: 'recently_created') do - Recently created - = link_to admin_users_path(sort: 'late_created') do - Late created - = link_to 'New User', new_admin_user_path, class: "btn btn-new" + = link_to admin_users_path(sort: sort_value_name) do + = sort_title_name + = link_to admin_users_path(sort: sort_value_recently_signin) do + = sort_title_recently_signin + = link_to admin_users_path(sort: sort_value_oldest_signin) do + = sort_title_oldest_signin + = link_to admin_users_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to admin_users_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to admin_users_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to admin_users_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated + + = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm" %ul.well-list - @users.each do |user| %li @@ -71,11 +79,12 @@ %i.fa.fa-envelope = mail_to user.email, user.email, class: 'light' - = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-small" + = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-xs" - unless user == current_user - if user.blocked? - = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-small success" + = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success" - else - = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-small btn-remove" - = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All tickets linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-small btn-remove" + = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs btn-warning" + - if user.can_be_removed? + = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All tickets linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" = paginate @users, theme: "gitlab" diff --git a/app/views/admin/users/new.html.haml b/app/views/admin/users/new.html.haml index 8fbb757f424..bfc36ed7373 100644 --- a/app/views/admin/users/new.html.haml +++ b/app/views/admin/users/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New User" %h3.page-title New user %hr diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 211d77d5185..48cd22fc34b 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -1,3 +1,4 @@ +- page_title @user.name, "Users" %h3.page-title User: = @user.name @@ -20,6 +21,8 @@ %a{"data-toggle" => "tab", href: "#groups"} Groups %li %a{"data-toggle" => "tab", href: "#projects"} Projects + %li + %a{"data-toggle" => "tab", href: "#ssh-keys"} SSH keys .tab-content #account.tab-pane.active @@ -44,9 +47,17 @@ %li %span.light Secondary email: %strong= email.email - = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-tiny btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do + = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-xs btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do %i.fa.fa-times + %li.two-factor-status + %span.light Two-factor Authentication: + %strong{class: @user.two_factor_enabled? ? 'cgreen' : 'cred'} + - if @user.two_factor_enabled? + Enabled + - else + Disabled + %li %span.light Can create groups: %strong @@ -95,7 +106,7 @@ %li %span.light LDAP uid: %strong - = @user.extern_uid + = @user.ldap_identity.extern_uid - if @user.created_by %li @@ -106,45 +117,53 @@ .col-md-6 - unless @user == current_user - if @user.blocked? - .alert.alert-info - %h4 This user is blocked - %p Blocking user has the following effects: - %ul - %li User will not be able to login - %li User will not be able to access git repositories - %li User will be removed from joined projects and groups - %li Personal projects will be left - %li Owned groups will be left - %br - = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-new", data: { confirm: 'Are you sure?' } + .panel.panel-info + .panel-heading + This user is blocked + .panel-body + %p Blocking user has the following effects: + %ul + %li User will not be able to login + %li User will not be able to access git repositories + %li Personal projects will be left + %li Owned groups will be left + %br + = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - else - .alert.alert-warning - %h4 Block this user - %p Blocking user has the following effects: - %ul - %li User will not be able to login - %li User will not be able to access git repositories - %li User will be removed from joined projects and groups - %li Personal projects will be left - %li Owned groups will be left - %br - = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-remove" - - .alert.alert-danger - %h4 + .panel.panel-warning + .panel-heading + Block this user + .panel-body + %p Blocking user has the following effects: + %ul + %li User will not be able to login + %li User will not be able to access git repositories + %li User will be removed from joined projects and groups + %li Personal projects will be left + %li Owned groups will be left + %br + = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning" + + .panel.panel-danger + .panel-heading Remove user - %p Deleting a user has the following effects: - %ul - %li All user content like authored issues, snippets, comments will be removed - - rp = @user.personal_projects.count - - unless rp.zero? - %li #{pluralize rp, 'personal project'} will be removed and cannot be restored - - if @user.solo_owned_groups.present? - %li - Next groups with all content will be removed: - %strong #{@user.solo_owned_groups.map(&:name).join(', ')} - %br - = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" + .panel-body + - if @user.can_be_removed? + %p Deleting a user has the following effects: + %ul + %li All user content like authored issues, snippets, comments will be removed + - rp = @user.personal_projects.count + - unless rp.zero? + %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + %br + = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" + - else + - if @user.solo_owned_groups.present? + %p + This user is currently an owner in these groups: + %strong #{@user.solo_owned_groups.map(&:name).join(', ')} + %p + You must transfer ownership or delete these groups before you can delete this user. #profile.tab-pane .row @@ -168,15 +187,15 @@ .panel.panel-default .panel-heading Groups: %ul.well-list - - @user.group_members.each do |user_group| - - group = user_group.group + - @user.group_members.each do |group_member| + - group = group_member.group %li.group_member - %span{class: ("list-item-name" unless user_group.owner?)} + %span{class: ("list-item-name" unless group_member.owner?)} %strong= link_to group.name, admin_group_path(group) .pull-right - %span.light= user_group.human_access - - unless user_group.owner? - = link_to group_group_member_path(group, user_group), data: { confirm: remove_user_from_group_message(group, @user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do + %span.light= group_member.human_access + - unless group_member.owner? + = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-times.fa-inverse - else .nothing-here-block This user has no groups. @@ -201,19 +220,21 @@ .panel-heading Joined projects (#{@joined_projects.count}) %ul.well-list - @joined_projects.sort_by(&:name_with_namespace).each do |project| - - tm = project.team.find_tm(@user.id) + - member = project.team.find_member(@user.id) %li.project_member .list-item-name - = link_to admin_project_path(project), class: dom_class(project) do + = link_to admin_namespace_project_path(project.namespace, project), class: dom_class(project) do = project.name_with_namespace - - if tm + - if member .pull-right - - if tm.owner? + - if member.owner? %span.light Owner - else - %span.light= tm.human_access + %span.light= member.human_access - - if tm.respond_to? :project - = link_to project_team_member_path(project, @user), data: { confirm: remove_from_project_team_message(project, @user) }, remote: true, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from project' do + - if member.respond_to? :project + = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do %i.fa.fa-times + #ssh-keys.tab-pane + = render 'profiles/keys/key_table', admin: true diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index fdf96dd6f56..213b5d65b3c 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -1,9 +1,13 @@ -= render "events/event_last_push", event: @last_push -= render 'shared/event_filter' +.hidden-xs + = render "events/event_last_push", event: @last_push -- if @events.any? - .content_list -- else - .nothing-here-block Projects activity will be displayed here + - if current_user + %ul.nav.nav-pills.event_filter.pull-right + %li.pull-right + = link_to dashboard_path(:atom, { private_token: current_user.private_token }), class: 'rss-btn' do + %i.fa.fa-rss + = render 'shared/event_filter' + %hr +.content_list = spinner diff --git a/app/views/dashboard/_groups.html.haml b/app/views/dashboard/_groups.html.haml deleted file mode 100644 index 5460cf56f22..00000000000 --- a/app/views/dashboard/_groups.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -.panel.panel-default - .panel-heading.clearfix - = search_field_tag :filter_group, nil, placeholder: 'Filter by name', class: 'dash-filter form-control' - - if current_user.can_create_group? - = link_to new_group_path, class: "btn btn-new pull-right" do - %i.fa.fa-plus - New group - %ul.well-list.dash-list - - groups.each do |group| - %li.group-row - = link_to group_path(id: group.path), class: dom_class(group) do - = image_tag group_icon(group.path), class: "avatar s24" - %span.group-name.filter-title - = truncate(group.name, length: 35) - %span.arrow - %i.fa.fa-angle-right - - if groups.blank? - %li - .nothing-here-block You have no groups yet. diff --git a/app/views/dashboard/_project.html.haml b/app/views/dashboard/_project.html.haml deleted file mode 100644 index 89ed5102754..00000000000 --- a/app/views/dashboard/_project.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -= link_to project_path(project), class: dom_class(project) do - .dash-project-access-icon - = visibility_level_icon(project.visibility_level) - %span.str-truncated - %span.namespace-name - - if project.namespace - = project.namespace.human_name - \/ - %span.project-name.filter-title - = project.name - %span.arrow - %i.fa.fa-angle-right diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/_projects.html.haml index 3598425777f..d676576067c 100644 --- a/app/views/dashboard/_projects.html.haml +++ b/app/views/dashboard/_projects.html.haml @@ -1,24 +1,10 @@ .panel.panel-default .panel-heading.clearfix - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'dash-filter form-control' - - if current_user.can_create_project? - = link_to new_project_path, class: "btn btn-new pull-right" do - %i.fa.fa-plus - New project + .input-group + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' + - if current_user.can_create_project? + %span.input-group-btn + = link_to new_project_path, class: 'btn btn-success' do + New project - %ul.well-list.dash-list - - projects.each do |project| - %li.project-row - = render "project", project: project - - - if projects.blank? - %li - .nothing-here-block There are no projects here. - - if @projects_count > @projects_limit - %li.bottom - %span.light - #{@projects_limit} of #{pluralize(@projects_count, 'project')} displayed. - .pull-right - = link_to projects_dashboard_path do - Show all - %i.fa.fa-angle-right + = render 'shared/projects_list', projects: @projects, projects_limit: 20 diff --git a/app/views/dashboard/_projects_filter.html.haml b/app/views/dashboard/_projects_filter.html.haml deleted file mode 100644 index b65e882e693..00000000000 --- a/app/views/dashboard/_projects_filter.html.haml +++ /dev/null @@ -1,55 +0,0 @@ -%fieldset - %ul.nav.nav-pills.nav-stacked - = nav_tab :scope, nil do - = link_to projects_dashboard_filter_path(scope: nil) do - All - %span.pull-right - = current_user.authorized_projects.count - = nav_tab :scope, 'personal' do - = link_to projects_dashboard_filter_path(scope: 'personal') do - Personal - %span.pull-right - = current_user.personal_projects.count - = nav_tab :scope, 'joined' do - = link_to projects_dashboard_filter_path(scope: 'joined') do - Joined - %span.pull-right - = current_user.authorized_projects.joined(current_user).count - = nav_tab :scope, 'owned' do - = link_to projects_dashboard_filter_path(scope: 'owned') do - Owned - %span.pull-right - = current_user.owned_projects.count - -%fieldset - %legend Visibility - %ul.nav.nav-pills.nav-stacked.nav-small.visibility-filter - - Gitlab::VisibilityLevel.values.each do |level| - %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } - = link_to projects_dashboard_filter_path(visibility_level: level) do - = visibility_level_icon(level) - = visibility_level_label(level) - -- if @groups.present? - %fieldset - %legend Groups - %ul.nav.nav-pills.nav-stacked.nav-small - - @groups.each do |group| - %li{ class: (group.name == params[:group]) ? 'active' : 'light' } - = link_to projects_dashboard_filter_path(group: group.name) do - %i.fa.fa-folder-o - = group.name - %small.pull-right - = group.projects.count - - - -- if @tags.present? - %fieldset - %legend Tags - %ul.nav.nav-pills.nav-stacked.nav-small - - @tags.each do |tag| - %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } - = link_to projects_dashboard_filter_path(scope: params[:scope], tag: tag.name) do - %i.fa.fa-tag - = tag.name diff --git a/app/views/dashboard/_sidebar.html.haml b/app/views/dashboard/_sidebar.html.haml index add9eb7fa29..78f695be916 100644 --- a/app/views/dashboard/_sidebar.html.haml +++ b/app/views/dashboard/_sidebar.html.haml @@ -1,25 +1,3 @@ -%ul.nav.nav-tabs.dash-sidebar-tabs - %li.active - = link_to '#projects', 'data-toggle' => 'tab', id: 'sidebar-projects-tab' do - Projects - %span.badge= @projects_count - %li - = link_to '#groups', 'data-toggle' => 'tab', id: 'sidebar-groups-tab' do - Groups - %span.badge= @groups.count - -.tab-content - .tab-pane.active#projects - = render "projects", projects: @projects - .tab-pane#groups - = render "groups", groups: @groups - += render "dashboard/projects", projects: @projects .prepend-top-20 - %span.rss-icon - = link_to dashboard_path(:atom, { private_token: current_user.private_token }) do - %strong - %i.fa.fa-rss - News Feed - -%hr -= render 'shared/promo' + = render 'shared/promo' diff --git a/app/views/dashboard/_zero_authorized_projects.html.haml b/app/views/dashboard/_zero_authorized_projects.html.haml index 5d133cd8285..4e7d6639727 100644 --- a/app/views/dashboard/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/_zero_authorized_projects.html.haml @@ -1,10 +1,11 @@ +- publicish_project_count = Project.publicish(current_user).count %h3.page-title Welcome to GitLab! %p.light Self hosted Git management application. %hr %div .dashboard-intro-icon %i.fa.fa-bookmark-o - %div + .dashboard-intro-text %p.slead You don't have access to any projects right now. %br @@ -17,34 +18,36 @@ - if current_user.can_create_project? .link_holder = link_to new_project_path, class: "btn btn-new" do - New project » + %i.fa.fa-plus + New Project - if current_user.can_create_group? %hr %div .dashboard-intro-icon %i.fa.fa-users - %div + .dashboard-intro-text %p.slead You can create a group for several dependent projects. %br Groups are the best way to manage projects and members. .link_holder = link_to new_group_path, class: "btn btn-new" do - New group » + %i.fa.fa-plus + New Group --if @publicish_project_count > 0 +-if publicish_project_count > 0 %hr %div .dashboard-intro-icon %i.fa.fa-globe - %div + .dashboard-intro-text %p.slead There are - %strong= @publicish_project_count + %strong= publicish_project_count public projects on this server. %br Public projects are an easy way to allow everyone to have read-only access. .link_holder = link_to trending_explore_projects_path, class: "btn btn-new" do - Browse public projects » + Browse public projects diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml new file mode 100644 index 00000000000..0a354373b9b --- /dev/null +++ b/app/views/dashboard/groups/index.html.haml @@ -0,0 +1,40 @@ +- page_title "Groups" +%h3.page-title + Group Membership + - if current_user.can_create_group? + %span.pull-right.hidden-xs + = link_to new_group_path, class: "btn btn-new" do + %i.fa.fa-plus + New Group +%p.light + Group members have access to all group projects. +%hr +.panel.panel-default + .panel-heading + %strong Groups + (#{@group_members.count}) + %ul.well-list + - @group_members.each do |group_member| + - group = group_member.group + %li + .pull-right.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 + Settings + + = 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 + %i.fa.fa-sign-out + Leave + + = image_tag group_icon(group), class: "avatar s40 avatar-tile hidden-xs" + = link_to group, class: 'group-name' do + %strong= group.name + + as + %strong #{group_member.human_access} + + %div.light + #{pluralize(group.projects.count, "project")}, #{pluralize(group.users.count, "user")} + += paginate @group_members diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index f5413557783..07bda1c77f8 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -1,24 +1,13 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{current_user.name} issues" - xml.link :href => issues_dashboard_url(:atom, :private_token => current_user.private_token), :rel => "self", :type => "application/atom+xml" - xml.link :href => issues_dashboard_url(:private_token => current_user.private_token), :rel => "alternate", :type => "text/html" - xml.id issues_dashboard_url(:private_token => current_user.private_token) + xml.link href: issues_dashboard_url(format: :atom, private_token: current_user.try(: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.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? @issues.each do |issue| - xml.entry do - xml.id project_issue_url(issue.project, issue) - xml.link :href => project_issue_url(issue.project, issue) - xml.title truncate(issue.title, :length => 80) - xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, :width => "40", :height => "40", :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 + issue_to_atom(xml, issue) end end diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 7c1f1ddbb80..0dd2edbb1bc 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -1,3 +1,8 @@ +- page_title "Issues" += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues") + %h3.page-title Issues @@ -5,10 +10,12 @@ List all issues from all projects you have access to. %hr -.row - .fixed.sidebar-expand-button.hidden-lg.hidden-md - %i.fa.fa-list.fa-2x - .col-md-3.responsive-side - = render 'shared/filter', entity: 'issue' - .col-md-9 - = render 'shared/issues' +.append-bottom-20 + .pull-right + - if current_user + .hidden-xs.pull-left + = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do + %i.fa.fa-rss + + = render 'shared/issuable_filter', type: :issues += render 'shared/issues' diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index c96584c7b6b..61d2fbe538c 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -1,3 +1,4 @@ +- page_title "Merge Requests" %h3.page-title Merge Requests @@ -5,10 +6,6 @@ %p.light List all merge requests from all projects you have access to. %hr -.row - .fixed.sidebar-expand-button.hidden-lg.hidden-md - %i.fa.fa-list.fa-2x - .col-md-3.responsive-side - = render 'shared/filter', entity: 'merge_request' - .col-md-9 - = render 'shared/merge_requests' +.append-bottom-20 + = render 'shared/issuable_filter', type: :merge_requests += render 'shared/merge_requests' diff --git a/app/views/dashboard/milestones/_issue.html.haml b/app/views/dashboard/milestones/_issue.html.haml new file mode 100644 index 00000000000..f689b9698eb --- /dev/null +++ b/app/views/dashboard/milestones/_issue.html.haml @@ -0,0 +1,10 @@ +%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid } + %span.milestone-row + - project = issue.project + %strong #{project.name_with_namespace} · + = link_to [project.namespace.becomes(Namespace), project, issue] do + %span.cgray ##{issue.iid} + = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title + .pull-right.assignee-icon + - if issue.assignee + = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16" diff --git a/app/views/dashboard/milestones/_issues.html.haml b/app/views/dashboard/milestones/_issues.html.haml new file mode 100644 index 00000000000..9f350b772bd --- /dev/null +++ b/app/views/dashboard/milestones/_issues.html.haml @@ -0,0 +1,6 @@ +.panel.panel-default + .panel-heading= title + %ul{ class: "well-list issues-sortable-list" } + - if issues + - issues.each do |issue| + = render 'issue', issue: issue diff --git a/app/views/dashboard/milestones/_merge_request.html.haml b/app/views/dashboard/milestones/_merge_request.html.haml new file mode 100644 index 00000000000..8f5c4cce529 --- /dev/null +++ b/app/views/dashboard/milestones/_merge_request.html.haml @@ -0,0 +1,10 @@ +%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid } + %span.milestone-row + - project = merge_request.project + %strong #{project.name_with_namespace} · + = link_to [project.namespace.becomes(Namespace), project, merge_request] do + %span.cgray ##{merge_request.iid} + = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title + .pull-right.assignee-icon + - if merge_request.assignee + = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16" diff --git a/app/views/dashboard/milestones/_merge_requests.html.haml b/app/views/dashboard/milestones/_merge_requests.html.haml new file mode 100644 index 00000000000..50057e2c636 --- /dev/null +++ b/app/views/dashboard/milestones/_merge_requests.html.haml @@ -0,0 +1,6 @@ +.panel.panel-default + .panel-heading= title + %ul{ class: "well-list merge_requests-sortable-list" } + - if merge_requests + - merge_requests.each do |merge_request| + = render 'merge_request', merge_request: merge_request diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml new file mode 100644 index 00000000000..d6f3e029a38 --- /dev/null +++ b/app/views/dashboard/milestones/_milestone.html.haml @@ -0,0 +1,20 @@ +%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } + %h4 + = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) + .row + .col-sm-6 + = link_to issues_dashboard_path(milestone_title: milestone.title) do + = pluralize milestone.issue_count, 'Issue' + + = link_to merge_requests_dashboard_path(milestone_title: milestone.title) do + = pluralize milestone.merge_requests_count, 'Merge Request' + + %span.light #{milestone.percent_complete}% complete + + .col-sm-6 + = milestone_progress_bar(milestone) + %div + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label.label-gray + = milestone.project.name_with_namespace diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml new file mode 100644 index 00000000000..9a9a5e139a4 --- /dev/null +++ b/app/views/dashboard/milestones/index.html.haml @@ -0,0 +1,21 @@ +- page_title "Milestones" +%h3.page-title + Milestones + %span.pull-right #{@dashboard_milestones.count} milestones + +%p.light + List all milestones from all projects you have access to. + +%hr + += render 'shared/milestones_filter' +.milestones + .panel.panel-default + %ul.well-list + - if @dashboard_milestones.blank? + %li + .nothing-here-block No milestones to show + - else + - @dashboard_milestones.each do |milestone| + = render 'milestone', milestone: milestone + = paginate @dashboard_milestones, theme: "gitlab" diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml new file mode 100644 index 00000000000..0d204ced7ea --- /dev/null +++ b/app/views/dashboard/milestones/show.html.haml @@ -0,0 +1,85 @@ +- page_title @dashboard_milestone.title, "Milestones" +%h4.page-title + .issue-box{ class: "issue-box-#{@dashboard_milestone.closed? ? 'closed' : 'open'}" } + - if @dashboard_milestone.closed? + Closed + - else + Open + Milestone #{@dashboard_milestone.title} + +%hr +- if (@dashboard_milestone.total_items_count == @dashboard_milestone.closed_items_count) && @dashboard_milestone.active? + .alert.alert-success + %span All issues for this milestone are closed. You may close the milestone now. + +.description +%table.table + %thead + %tr + %th Project + %th Open issues + %th State + %th Due date + - @dashboard_milestone.milestones.each do |milestone| + %tr + %td + = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) + %td + = milestone.issues.opened.count + %td + - if milestone.closed? + Closed + - else + Open + %td + = milestone.expires_at + +.context + %p.lead + Progress: + #{@dashboard_milestone.closed_items_count} closed + – + #{@dashboard_milestone.open_items_count} open + = milestone_progress_bar(@dashboard_milestone) + +%ul.nav.nav-tabs + %li.active + = link_to '#tab-issues', 'data-toggle' => 'tab' do + Issues + %span.badge= @dashboard_milestone.issue_count + %li + = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do + Merge Requests + %span.badge= @dashboard_milestone.merge_requests_count + %li + = link_to '#tab-participants', 'data-toggle' => 'tab' do + Participants + %span.badge= @dashboard_milestone.participants.count + + .pull-right + = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @dashboard_milestone.title), class: "btn edit-milestone-link btn-grouped" + +.tab-content + .tab-pane.active#tab-issues + .row + .col-md-6 + = render 'issues', title: "Open", issues: @dashboard_milestone.opened_issues + .col-md-6 + = render 'issues', title: "Closed", issues: @dashboard_milestone.closed_issues + + .tab-pane#tab-merge-requests + .row + .col-md-6 + = render 'merge_requests', title: "Open", merge_requests: @dashboard_milestone.opened_merge_requests + .col-md-6 + = render 'merge_requests', title: "Closed", merge_requests: @dashboard_milestone.closed_merge_requests + + .tab-pane#tab-participants + %ul.bordered-list + - @dashboard_milestone.participants.each do |user| + %li + = link_to user, title: user.name, class: "darken" do + = image_tag avatar_icon(user.email, 32), class: "avatar s32" + %strong= truncate(user.name, lenght: 40) + %br + %small.cgray= user.username diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml deleted file mode 100644 index f124c688be1..00000000000 --- a/app/views/dashboard/projects.html.haml +++ /dev/null @@ -1,73 +0,0 @@ -%h3.page-title - My Projects -.pull-right - .dropdown.inline - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} - %span.light sort: - - if @sort.present? - = @sort.humanize - - else - Name - %b.caret - %ul.dropdown-menu - %li - = link_to projects_dashboard_filter_path(sort: nil) do - Name - = link_to projects_dashboard_filter_path(sort: 'newest') do - Newest - = link_to projects_dashboard_filter_path(sort: 'oldest') do - Oldest - = link_to projects_dashboard_filter_path(sort: 'recently_updated') do - Recently updated - = link_to projects_dashboard_filter_path(sort: 'last_updated') do - Last updated -%p.light - All projects you have access to are listed here. Public projects are not included here unless you are a member -%hr -.row - .col-md-3.hidden-sm.hidden-xs.side-filters - = render "projects_filter" - .col-md-9 - %ul.bordered-list.my-projects.top-list - - @projects.each do |project| - %li.my-project-row - %h4.project-title - .project-access-icon - = visibility_level_icon(project.visibility_level) - = link_to project_path(project), class: dom_class(project) do - = project.name_with_namespace - - - if current_user.can_leave_project?(project) - .pull-right - = link_to leave_project_team_members_path(project), data: { confirm: "Leave project?"}, method: :delete, remote: true, class: "btn-tiny btn remove-row", title: 'Leave project' do - %i.fa.fa-sign-out - Leave - - - if project.forked_from_project - %small.pull-right - %i.fa.fa-code-fork - Forked from: - = link_to project.forked_from_project.name_with_namespace, project_path(project.forked_from_project) - .project-info - .pull-right - - if project.archived? - %span.label - %i.fa.fa-archive - Archived - - project.tags.each do |tag| - %span.label.label-info - %i.fa.fa-tag - = tag.name - - if project.description.present? - %p= truncate project.description, length: 100 - .last-activity - %span.light Last activity: - %span.date= project_last_activity(project) - - - - if @projects.blank? - %li - .nothing-here-block There are no projects here. - .bottom - = paginate @projects, theme: "gitlab" - diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml new file mode 100644 index 00000000000..8aaa0a7f071 --- /dev/null +++ b/app/views/dashboard/projects/starred.html.haml @@ -0,0 +1,23 @@ +- page_title "Starred Projects" +- if @projects.any? + = render 'shared/show_aside' + + .dashboard.row + %section.activities.col-md-8 + = render 'dashboard/activities' + %aside.col-md-4 + .panel.panel-default + .panel-heading.clearfix + .input-group + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' + - if current_user.can_create_project? + %span.input-group-btn + = link_to new_project_path, class: 'btn btn-success' do + New project + + = render 'shared/projects_list', projects: @projects, + projects_limit: 20, stars: true, avatar: false + +- else + %h3 You don't have starred projects yet + %p.slead Visit project page and press on star icon and it will appear on this page. diff --git a/app/views/dashboard/show.atom.builder b/app/views/dashboard/show.atom.builder index f4cf24ccd99..e9a612231d5 100644 --- a/app/views/dashboard/show.atom.builder +++ b/app/views/dashboard/show.atom.builder @@ -1,29 +1,12 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "Dashboard feed#{" - #{current_user.name}" if current_user.name.present?}" - xml.link :href => dashboard_url(:atom), :rel => "self", :type => "application/atom+xml" - xml.link :href => dashboard_url, :rel => "alternate", :type => "text/html" - xml.id projects_url + xml.title "Activity" + xml.link href: dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: dashboard_url, rel: "alternate", type: "text/html" + xml.id dashboard_url xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| - if event.proper? - 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.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, :width => "40", :height => "40", :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 + event_to_atom(xml, event) end end diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/show.html.haml index 10951af6a09..5001c2101e1 100644 --- a/app/views/dashboard/show.html.haml +++ b/app/views/dashboard/show.html.haml @@ -1,12 +1,15 @@ -- if @has_authorized_projects += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, dashboard_url(format: :atom, private_token: current_user.private_token), title: "All activity") + +- if @projects.any? + = render 'shared/show_aside' + .dashboard.row %section.activities.col-md-8 = render 'activities' - %aside.side.col-md-4.left.responsive-side + %aside.col-md-4 = render 'sidebar' - .fixed.sidebar-expand-button.hidden-lg.hidden-md - %i.fa.fa-list.fa-2x - - else = render "zero_authorized_projects" diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml index 8d17f39eba2..970ba147111 100755..100644 --- a/app/views/devise/confirmations/new.html.haml +++ b/app/views/devise/confirmations/new.html.haml @@ -7,7 +7,8 @@ = devise_error_messages! .clearfix.append-bottom-20 = f.email_field :email, placeholder: 'Email', class: "form-control", required: true - .clearfix.append-bottom-10 + .clearfix = f.submit "Resend confirmation instructions", class: 'btn btn-success' - .login-footer - = render 'devise/shared/sign_in_link' + +.clearfix.prepend-top-20 + = render 'devise/shared/sign_in_link' diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb index cb1291cf3bf..c6fa8f0ee36 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.erb +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -6,4 +6,4 @@ <p>You can confirm your account through the link below:</p> <% end %> -<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p> +<p><%= link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) %></p> diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb index 7913e88beb6..23b31da92d8 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.erb +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -2,7 +2,7 @@ <p>Someone has requested a link to change your password, and you can do this through the link below.</p> -<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p> +<p><%= link_to 'Change your password', edit_password_url(@resource, reset_password_token: @token) %></p> <p>If you didn't request this, please ignore this email.</p> <p>Your password won't change until you access the link above and create a new one.</p> diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb index 8c2a4f0c2d9..79d6c761d8f 100644 --- a/app/views/devise/mailer/unlock_instructions.html.erb +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -4,4 +4,4 @@ <p>Click the link below to unlock your account:</p> -<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p> +<p><%= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) %></p> diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index f6cbf9b82ba..56048e99c17 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -6,13 +6,14 @@ .devise-errors = devise_error_messages! = f.hidden_field :reset_password_token - .form-group#password-strength - = f.password_field :password, class: "form-control top", id: "user_password_recover", placeholder: "New password", required: true + %div + = f.password_field :password, class: "form-control top", placeholder: "New password", required: true %div = f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm new password", required: true - .clearfix.append-bottom-10 - = f.submit "Change my password", class: "btn btn-primary" - .login-footer - %p - = link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) - = render 'devise/shared/sign_in_link' + .clearfix + = f.submit "Change your password", class: "btn btn-primary" + +.clearfix.prepend-top-20 + %p + = link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) + = render 'devise/shared/sign_in_link' diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index b8af1b8693a..29ffe8a8be3 100755..100644 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -6,8 +6,9 @@ .devise-errors = devise_error_messages! .clearfix.append-bottom-20 - = f.email_field :email, placeholder: "Email", class: "form-control", required: true - .clearfix.append-bottom-10 + = f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email] + .clearfix = f.submit "Reset password", class: "btn-primary btn" - .login-footer - = render 'devise/shared/sign_in_link' + +.clearfix.prepend-top-20 + = render 'devise/shared/sign_in_link' diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index b11817af95d..f379e71ae5b 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -21,8 +21,8 @@ <div><%= f.submit "Update", class: "input_button" %></div> <% end %> -<h3>Cancel my account</h3> +<h3>Cancel your account</h3> -<p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>.</p> +<p>Unhappy? <%= link_to "Cancel your account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>.</p> <%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml index 123de881f59..42cfbbf84f2 100644 --- a/app/views/devise/registrations/new.html.haml +++ b/app/views/devise/registrations/new.html.haml @@ -1,27 +1,4 @@ -.login-box - .login-heading - %h3 Sign up - .login-body - = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| - .devise-errors - = devise_error_messages! - %div - = f.text_field :name, class: "form-control top", placeholder: "Name", required: true - %div - = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true - %div - = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true - .form-group#password-strength - = f.password_field :password, class: "form-control middle", id: "user_password_sign_up", placeholder: "Password", required: true - %div - = f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm password", required: true - %div - = f.submit "Sign up", class: "btn-create btn" - .login-footer - %p - %span.light - Have an account? - %strong - = link_to "Sign in", new_session_path(resource_name) - %p - = link_to "Forgot your password?", new_password_path(resource_name) +- page_title "Sign up" += render 'devise/shared/signup_box' + += render 'devise/shared/sign_in_link' diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index e819847e5ea..54a39726771 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -2,11 +2,11 @@ = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus" = f.password_field :password, class: "form-control bottom", placeholder: "Password" - if devise_mapping.rememberable? - .clearfix.append-bottom-10 - %label.checkbox.remember_me{for: "user_remember_me"} + .remember-me.checkbox + %label{for: "user_remember_me"} = f.check_box :remember_me %span Remember me - .pull-right - = link_to "Forgot your password?", new_password_path(resource_name) + .pull-right + = link_to "Forgot your password?", new_password_path(resource_name) %div = f.submit "Sign in", class: "btn btn-save" diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index bf8a593c254..6ec741e4882 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,5 +1,9 @@ -= form_tag(user_omniauth_callback_path(provider), id: 'new_ldap_user' ) do - = text_field_tag :username, nil, {class: "form-control top", placeholder: "LDAP Login", autofocus: "autofocus"} += form_tag(user_omniauth_callback_path(server['provider_name']), id: 'new_ldap_user' ) do + = text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"} = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} - %br/ - = button_tag "LDAP Sign in", class: "btn-save btn" + - if devise_mapping.rememberable? + .remember-me.checkbox + %label{for: "remember_me"} + = check_box_tag :remember_me, '1', false, id: 'remember_me' + %span Remember me + = button_tag "#{server['label']} Sign in", class: "btn-save btn" diff --git a/app/views/devise/sessions/_oauth_providers.html.haml b/app/views/devise/sessions/_oauth_providers.html.haml deleted file mode 100644 index 15048a78063..00000000000 --- a/app/views/devise/sessions/_oauth_providers.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- providers = (enabled_oauth_providers - [:ldap]) -- if providers.present? - .bs-callout.bs-callout-info{:'data-no-turbolink' => 'data-no-turbolink'} - %span Sign in with: - - providers.each do |provider| - %span - - if default_providers.include?(provider) - = link_to authbutton(provider, 32), omniauth_authorize_path(resource_name, provider) - - else - = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn" diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index ca7e9570b43..dbc8eda6196 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,43 +1,19 @@ -.login-box - .login-heading - %h3 Sign in - .login-body - - if ldap_enabled? - %ul.nav.nav-tabs - - @ldap_servers.each_with_index do |server, i| - %li{class: (:active if i.zero?)} - = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' - - if gitlab_config.signin_enabled - %li - = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' - .tab-content - - @ldap_servers.each_with_index do |server, i| - %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero?)} - = render 'devise/sessions/new_ldap', provider: server['provider_name'] - - if gitlab_config.signin_enabled - %div#tab-signin.tab-pane - = render 'devise/sessions/new_base' +- page_title "Sign in" +%div + - if signin_enabled? || ldap_enabled? + = render 'devise/shared/signin_box' - - elsif gitlab_config.signin_enabled - = render 'devise/sessions/new_base' - - else - %div - No authentication methods configured. + -# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box + - if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable? + .clearfix.prepend-top-20 + = render 'devise/shared/omniauth_box' - = render 'devise/sessions/oauth_providers' if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable? + -# Signup only makes sense if you can also sign-in + - if signin_enabled? && signup_enabled? + .prepend-top-20 + = render 'devise/shared/signup_box' - .login-footer - - if gitlab_config.signup_enabled - %p - %span.light - Don't have an account? - %strong - = link_to "Sign up", new_registration_path(resource_name) - - %p - %span.light Did not receive confirmation email? - = link_to "Send again", new_confirmation_path(resource_name) - - - if extra_config.has_key?('sign_in_text') - %hr - = markdown(extra_config.sign_in_text) + -# Show a message if none of the mechanisms above are enabled + - if !signin_enabled? && !ldap_enabled? && !(Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?) + %div + No authentication methods configured. diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml new file mode 100644 index 00000000000..22b2c1a186b --- /dev/null +++ b/app/views/devise/sessions/two_factor.html.haml @@ -0,0 +1,10 @@ +%div + .login-box + .login-heading + %h3 Two-factor Authentication + .login-body + = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| + = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor authentication code', required: true, autofocus: true + %p.help-block.hint If you've lost your phone, you may enter one of your recovery codes. + .prepend-top-20 + = f.submit "Verify code", class: "btn btn-save" diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml new file mode 100644 index 00000000000..f8ba9d80ae8 --- /dev/null +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -0,0 +1,10 @@ +%p + %span.light + Sign in with + - providers = additional_providers + - providers.each do |provider| + %span.light + - if default_providers.include?(provider) + = link_to oauth_image_tag(provider), omniauth_authorize_path(resource_name, provider), method: :post, class: 'oauth-image-link' + - else + = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), method: :post, class: "btn", "data-no-turbolink" => "true" diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml new file mode 100644 index 00000000000..c76574db457 --- /dev/null +++ b/app/views/devise/shared/_signin_box.html.haml @@ -0,0 +1,26 @@ +.login-box + - if signup_enabled? + .login-heading + %h3 Existing user? Sign in + - else + .login-heading + %h3 Sign in + .login-body + - if ldap_enabled? + %ul.nav.nav-tabs + - @ldap_servers.each_with_index do |server, i| + %li{class: (:active if i.zero?)} + = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' + - if signin_enabled? + %li + = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' + .tab-content + - @ldap_servers.each_with_index do |server, i| + %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero?)} + = render 'devise/sessions/new_ldap', server: server + - if signin_enabled? + %div#tab-signin.tab-pane + = render 'devise/sessions/new_base' + + - elsif signin_enabled? + = render 'devise/sessions/new_base' diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml new file mode 100644 index 00000000000..9dc6aeffd59 --- /dev/null +++ b/app/views/devise/shared/_signup_box.html.haml @@ -0,0 +1,27 @@ +.login-box + - if signin_enabled? + .login-heading + %h3 New user? Create an account + - else + .login-heading + %h3 Create an account + .login-body + = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| + .devise-errors + = devise_error_messages! + %div + = f.text_field :name, class: "form-control top", placeholder: "Name", required: true + %div + = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true + %div + = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true + .form-group.append-bottom-20#password-strength + = f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true + %div + = f.submit "Sign up", class: "btn-create btn" + +.clearfix.prepend-top-20 + %p + %span.light Didn't receive a confirmation email? + = succeed '.' do + = link_to "Request a new one", new_confirmation_path(resource_name) diff --git a/app/views/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml new file mode 100644 index 00000000000..6a5c917049d --- /dev/null +++ b/app/views/doorkeeper/applications/_delete_form.html.haml @@ -0,0 +1,4 @@ +- submit_btn_css ||= 'btn btn-link btn-remove btn-sm' += form_tag oauth_application_path(application) do + %input{:name => "_method", :type => "hidden", :value => "delete"}/ + = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
\ No newline at end of file diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml new file mode 100644 index 00000000000..98a61ab211b --- /dev/null +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -0,0 +1,30 @@ += form_for application, url: doorkeeper_submit_path(application), html: {class: 'form-horizontal', role: 'form'} do |f| + - if application.errors.any? + .alert.alert-danger + %ul + - application.errors.full_messages.each do |msg| + %li= msg + + .form-group + = f.label :name, class: 'control-label' + + .col-sm-10 + = f.text_field :name, class: 'form-control', required: true + + .form-group + = f.label :redirect_uri, class: 'control-label' + + .col-sm-10 + = f.text_area :redirect_uri, class: 'form-control', required: true + + %span.help-block + Use one line per URI + - if Doorkeeper.configuration.native_redirect_uri + %span.help-block + Use + %code= Doorkeeper.configuration.native_redirect_uri + for local tests + + .form-actions + = f.submit 'Submit', class: "btn btn-create" + = link_to "Cancel", applications_profile_path, class: "btn btn-cancel" diff --git a/app/views/doorkeeper/applications/edit.html.haml b/app/views/doorkeeper/applications/edit.html.haml new file mode 100644 index 00000000000..fb6aa30acee --- /dev/null +++ b/app/views/doorkeeper/applications/edit.html.haml @@ -0,0 +1,3 @@ +- page_title "Edit", @application.name, "Applications" +%h3.page-title Edit application += render 'form', application: @application diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml new file mode 100644 index 00000000000..3b0b19107ca --- /dev/null +++ b/app/views/doorkeeper/applications/index.html.haml @@ -0,0 +1,17 @@ +- page_title "Applications" +%h3.page-title Your applications +%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success' +%table.table.table-striped + %thead + %tr + %th Name + %th Callback URL + %th + %th + %tbody + - @applications.each do |application| + %tr{:id => "application_#{application.id}"} + %td= link_to application.name, oauth_application_path(application) + %td= application.redirect_uri + %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link' + %td= render 'delete_form', application: application diff --git a/app/views/doorkeeper/applications/new.html.haml b/app/views/doorkeeper/applications/new.html.haml new file mode 100644 index 00000000000..fd32a468b45 --- /dev/null +++ b/app/views/doorkeeper/applications/new.html.haml @@ -0,0 +1,7 @@ +- page_title "New Application" + +%h3.page-title New Application + +%hr + += render 'form', application: @application
\ No newline at end of file diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml new file mode 100644 index 00000000000..80340aca54c --- /dev/null +++ b/app/views/doorkeeper/applications/show.html.haml @@ -0,0 +1,27 @@ +- page_title @application.name, "Applications" +%h3.page-title + Application: #{@application.name} + + +%table.table + %tr + %td + Application Id + %td + %code#application_id= @application.uid + %tr + %td + Secret: + %td + %code#secret= @application.secret + + %tr + %td + Callback url + %td + - @application.redirect_uri.split.each do |uri| + %div + %span.monospace= uri +.form-actions + = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left' + = render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10' diff --git a/app/views/doorkeeper/authorizations/error.html.haml b/app/views/doorkeeper/authorizations/error.html.haml new file mode 100644 index 00000000000..7561ec85ed9 --- /dev/null +++ b/app/views/doorkeeper/authorizations/error.html.haml @@ -0,0 +1,3 @@ +%h3.page-title An error has occurred +%main{:role => "main"} + %pre= @pre_auth.error_response.body[:error_description]
\ No newline at end of file diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml new file mode 100644 index 00000000000..15f9ee266c1 --- /dev/null +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -0,0 +1,28 @@ +%h3.page-title Authorize required +%main{:role => "main"} + %p.h4 + Authorize + %strong.text-info= @pre_auth.client.name + to use your account? + - if @pre_auth.scopes + #oauth-permissions + %p This application will be able to: + %ul.text-info + - @pre_auth.scopes.each do |scope| + %li= t scope, scope: [:doorkeeper, :scopes] + %hr/ + .actions + = form_tag oauth_authorization_path, method: :post do + = hidden_field_tag :client_id, @pre_auth.client.uid + = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri + = hidden_field_tag :state, @pre_auth.state + = hidden_field_tag :response_type, @pre_auth.response_type + = hidden_field_tag :scope, @pre_auth.scope + = submit_tag "Authorize", class: "btn btn-success wide pull-left" + = form_tag oauth_authorization_path, method: :delete do + = hidden_field_tag :client_id, @pre_auth.client.uid + = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri + = hidden_field_tag :state, @pre_auth.state + = hidden_field_tag :response_type, @pre_auth.response_type + = hidden_field_tag :scope, @pre_auth.scope + = submit_tag "Deny", class: "btn btn-danger prepend-left-10"
\ No newline at end of file diff --git a/app/views/doorkeeper/authorizations/show.html.haml b/app/views/doorkeeper/authorizations/show.html.haml new file mode 100644 index 00000000000..9a402007194 --- /dev/null +++ b/app/views/doorkeeper/authorizations/show.html.haml @@ -0,0 +1,3 @@ +%h3.page-title Authorization code: +%main{:role => "main"} + %code#authorization_code= params[:code]
\ No newline at end of file diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml new file mode 100644 index 00000000000..4bba72167e3 --- /dev/null +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml @@ -0,0 +1,4 @@ +- submit_btn_css ||= 'btn btn-link btn-remove' += form_tag oauth_authorized_application_path(application) do + %input{:name => "_method", :type => "hidden", :value => "delete"}/ + = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-sm'
\ No newline at end of file diff --git a/app/views/doorkeeper/authorized_applications/index.html.haml b/app/views/doorkeeper/authorized_applications/index.html.haml new file mode 100644 index 00000000000..814cdc987ef --- /dev/null +++ b/app/views/doorkeeper/authorized_applications/index.html.haml @@ -0,0 +1,16 @@ +%header.page-header + %h1 Your authorized applications +%main{:role => "main"} + %table.table.table-striped + %thead + %tr + %th Application + %th Created At + %th + %th + %tbody + - @applications.each do |application| + %tr + %td= application.name + %td= application.created_at.strftime('%Y-%m-%d %H:%M:%S') + %td= render 'delete_form', application: application
\ No newline at end of file diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index a1d8664c4ce..012e9857642 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -1,3 +1,4 @@ +- page_title "Access Denied" %h1 403 %h3 Access Denied %hr diff --git a/app/views/errors/encoding.html.haml b/app/views/errors/encoding.html.haml index 64c7451a8da..90cfbebfcc6 100644 --- a/app/views/errors/encoding.html.haml +++ b/app/views/errors/encoding.html.haml @@ -1,3 +1,4 @@ +- page_title "Encoding Error" %h1 500 %h3 Encoding Error %hr diff --git a/app/views/errors/git_not_found.html.haml b/app/views/errors/git_not_found.html.haml index 189e53bca55..ff5d4cc1506 100644 --- a/app/views/errors/git_not_found.html.haml +++ b/app/views/errors/git_not_found.html.haml @@ -1,3 +1,4 @@ +- page_title "Git Resource Not Found" %h1 404 %h3 Git Resource Not found %hr diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml index 7bf88f592cf..3756b98ebb2 100644 --- a/app/views/errors/not_found.html.haml +++ b/app/views/errors/not_found.html.haml @@ -1,3 +1,4 @@ +- page_title "Not Found" %h1 404 %h3 The resource you were looking for doesn't exist. %hr diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml index f3c8221a9d9..3e70e98a24c 100644 --- a/app/views/errors/omniauth_error.html.haml +++ b/app/views/errors/omniauth_error.html.haml @@ -1,3 +1,4 @@ +- page_title "Auth Error" %h1 422 %h3 Sign-in using #{@provider} auth failed %hr diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index f0c34def145..742b74a67c7 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -1,5 +1,5 @@ %li.commit .commit-row-title - = link_to truncate_sha(commit[:id]), project_commit_path(project, commit[:id]), class: "commit_short_id", alt: '' + = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '' - = gfm event_commit_title(commit[:message]), project + = gfm event_commit_title(commit[:message]), project: project diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 61383315373..02b1dec753c 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -3,13 +3,14 @@ .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} - = cache event do + = cache [event, current_user] do = image_tag avatar_icon(event.author_email, 24), class: "avatar s24", alt:'' - if event.push? = render "events/event/push", event: event - - elsif event.note? + - elsif event.commented? = render "events/event/note", event: event + - elsif event.created_project? + = render "events/event/created_project", event: event - else - = render "events/event/common", event: event - + = render "events/event/common", event: event
\ No newline at end of file diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml index eba2b63797a..4259f64c191 100644 --- a/app/views/events/_event_issue.atom.haml +++ b/app/views/events/_event_issue.atom.haml @@ -1,3 +1,3 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - if issue.description.present? - = markdown issue.description + = markdown(issue.description, xhtml: true, reference_only_path: false, project: issue.project) diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index 4c9a39bcc27..501412642db 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -2,13 +2,13 @@ .event-last-push .event-last-push-text %span You pushed to - = link_to project_commits_path(event.project, event.ref_name) do + = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do %strong= event.ref_name - at + %span at %strong= link_to_project event.project #{time_ago_with_tooltip(event.created_at)} .pull-right - = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-create btn-small" do + = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-create btn-sm" do Create Merge Request %hr diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml index 0aea2d17d65..e8ed13df783 100644 --- a/app/views/events/_event_merge_request.atom.haml +++ b/app/views/events/_event_merge_request.atom.haml @@ -1,3 +1,3 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - if merge_request.description.present? - = markdown merge_request.description + = markdown(merge_request.description, xhtml: true, reference_only_path: false, project: merge_request.project) diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml index be0e05481ed..cfbfba50202 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 + = markdown(note.note, xhtml: true, reference_only_path: false, project: note.project) diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index 2b63519edac..3625cb49d8b 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -2,11 +2,11 @@ - event.commits.first(15).each do |commit| %p %strong= commit[:author][:name] - = link_to "(##{truncate_sha(commit[:id])})", project_commit_path(event.project, id: commit[:id]) + = link_to "(##{truncate_sha(commit[:id])})", namespace_project_commit_path(event.project.namespace, event.project, id: commit[:id]) %i at = commit[:timestamp].to_time.to_s(:short) - %blockquote= markdown(escape_once(commit[:message])) + %blockquote= markdown(escape_once(commit[:message]), xhtml: true, reference_only_path: false, project: event.project) - if event.commits_count > 15 %p %i diff --git a/app/views/events/_events.html.haml b/app/views/events/_events.html.haml index 3d62d478869..68c19df092d 100644 --- a/app/views/events/_events.html.haml +++ b/app/views/events/_events.html.haml @@ -1 +1 @@ -= render @events += render partial: 'events/event', collection: @events diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index a9d3adf41df..a39e62e9dac 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,15 +1,17 @@ .event-title %span.author_name= link_to_author event - %span.event_label{class: event.action_name}= event_action_name(event) + %span.event_label{class: event.action_name} + = event_action_name(event) + - if event.target - %strong= link_to "##{event.target_iid}", [event.project, event.target] - - else - %strong= gfm event.target_title - at + %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target] + at + - if event.project = link_to_project event.project - else = event.project_name + - if event.target.respond_to?(:title) .event-body .event-note diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml new file mode 100644 index 00000000000..c2577a24982 --- /dev/null +++ b/app/views/events/event/_created_project.html.haml @@ -0,0 +1,27 @@ +.event-title + %span.author_name= link_to_author event + %span.event_label{class: event.action_name} + = event_action_name(event) + + - if event.project + = link_to_project event.project + - else + = event.project_name + +- if current_user == event.author && !event.project.private? && twitter_sharing_enabled? + .event-body + .event-note + .md + %p + Congratulations! Why not share your accomplishment with the world? + + %a.twitter-share-button{ | + href: "https://twitter.com/share", | + "data-url" => event.project.web_url, | + "data-text" => "I just #{event.action_name} a new project on GitLab! GitLab is version control on your server.", | + "data-size" => "medium", | + "data-related" => "gitlab", | + "data-hashtags" => "gitlab", | + "data-count" => "none"} + Tweet + %script{src: "//platform.twitter.com/widgets.js"} diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index 6ec8e54fba5..07bec1697f5 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -1,6 +1,10 @@ .event-title %span.author_name= link_to_author event - %span.event_label commented on #{event_note_title_html(event)} at + %span.event_label + = event.action_name + = event_note_title_html(event) + at + - if event.project = link_to_project event.project - else @@ -10,13 +14,13 @@ .event-note .md %i.fa.fa-comment-o.event-note-icon - = event_note(event.target.note) + = event_note(event.target.note, project: event.project) - note = event.target - if note.attachment.url - if note.attachment.image? - = link_to note.attachment.secure_url, target: '_blank' do - = image_tag note.attachment.secure_url, class: 'note-image-attach' + = link_to note.attachment.url, target: '_blank' do + = image_tag note.attachment.url, class: 'note-image-attach' - else - = link_to note.attachment.secure_url, target: "_blank", class: 'note-file-attach' do + = link_to note.attachment.url, target: "_blank", class: 'note-file-attach' do %i.fa.fa-paperclip = note.attachment_identifier diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index b912b5e092f..34a7c00dc43 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -1,11 +1,11 @@ .event-title %span.author_name= link_to_author event - %span.event_label.pushed #{event.push_action_name} #{event.ref_type} + %span.event_label.pushed #{event.action_name} #{event.ref_type} - if event.rm_ref? %strong= event.ref_name - else - = link_to project_commits_path(event.project, event.ref_name) do - %strong= event.ref_name + %strong + = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) at = link_to_project event.project @@ -17,9 +17,27 @@ - few_commits.each do |commit| = render "events/commit", commit: commit, project: project + - create_mr = current_user == event.author && event.new_ref? && create_mr_button?(event.project.default_branch, event.ref_name, event.project) - if event.commits_count > 1 %li.commits-stat - if event.commits_count > 2 %span ... and #{event.commits_count - 2} more commits. - = link_to project_compare_path(event.project, from: event.commit_from, to: event.commit_to) do - %strong Compare → #{truncate_sha(event.commit_from)}...#{truncate_sha(event.commit_to)} + + - if event.md_ref? + - from = event.commit_from + - from_label = truncate_sha(from) + - else + - from = event.project.default_branch + - from_label = from + + = link_to namespace_project_compare_path(event.project.namespace, event.project, from: from, to: event.commit_to) do + Compare #{from_label}...#{truncate_sha(event.commit_to)} + + - if create_mr + or + = link_to create_mr_path(event.project.default_branch, event.ref_name, event.project) do + create a merge request + - elsif create_mr + %li.commits-stat + = link_to create_mr_path(event.project.default_branch, event.ref_name, event.project) do + Create Merge Request diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 709d062df83..f3f0b778539 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -1,32 +1,32 @@ +- page_title "Groups" .clearfix .pull-left = form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f| + = hidden_field_tag :sort, @sort .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "groups_search" + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search" .form-group = button_tag 'Search', class: "btn btn-primary wide" .pull-right .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} %span.light sort: - if @sort.present? - = @sort.humanize + = sort_options_hash[@sort] - else - Name + = sort_title_recently_created %b.caret %ul.dropdown-menu %li - = link_to explore_groups_path(sort: nil) do - Name - = link_to explore_groups_path(sort: 'newest') do - Newest - = link_to explore_groups_path(sort: 'oldest') do - Oldest - = link_to explore_groups_path(sort: 'recently_updated') do - Recently updated - = link_to explore_groups_path(sort: 'last_updated') do - Last updated + = link_to explore_groups_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to explore_groups_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to explore_groups_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to explore_groups_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated %hr diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml new file mode 100644 index 00000000000..82622a58ed2 --- /dev/null +++ b/app/views/explore/projects/_filter.html.haml @@ -0,0 +1,67 @@ +.pull-left + = form_tag explore_projects_filter_path, method: :get, class: 'form-inline form-tiny' do |f| + .form-group + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search" + .form-group + = button_tag 'Search', class: "btn btn-primary wide" + +.pull-right.hidden-sm.hidden-xs + - if current_user + .dropdown.inline.append-right-10 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-globe + %span.light Visibility: + - if params[:visibility_level].present? + = visibility_level_label(params[:visibility_level].to_i) + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to explore_projects_filter_path(visibility_level: nil) do + Any + - Gitlab::VisibilityLevel.values.each do |level| + %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } + = link_to explore_projects_filter_path(visibility_level: level) do + = visibility_level_icon(level) + = visibility_level_label(level) + + - if @tags.present? + .dropdown.inline.append-right-10 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-tags + %span.light Tags: + - if params[:tag].present? + = params[:tag] + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to explore_projects_filter_path(tag: nil) do + Any + + - @tags.each do |tag| + %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } + = link_to explore_projects_filter_path(tag: tag.name) do + %i.fa.fa-tag + = tag.name + + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light sort: + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_recently_created + %b.caret + %ul.dropdown-menu + %li + = link_to explore_projects_filter_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to explore_projects_filter_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to explore_projects_filter_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated diff --git a/app/views/explore/projects/_project.html.haml b/app/views/explore/projects/_project.html.haml index ffbddbae4d6..d65fb529373 100644 --- a/app/views/explore/projects/_project.html.haml +++ b/app/views/explore/projects/_project.html.haml @@ -2,12 +2,10 @@ %h4.project-title .project-access-icon = visibility_level_icon(project.visibility_level) - = link_to project.name_with_namespace, project - - - if current_page?(starred_explore_projects_path) - %strong.pull-right - %i.fa.fa-star - = pluralize project.star_count, 'star' + = link_to project.name_with_namespace, [project.namespace.becomes(Namespace), project] + %span.pull-right + %i.fa.fa-star + = project.star_count .project-info - if project.description.present? @@ -16,11 +14,11 @@ .repo-info - unless project.empty_repo? - = link_to pluralize(project.repository.round_commit_count, 'commit'), project_commits_path(project, project.default_branch) + = link_to pluralize(project.repository.round_commit_count, 'commit'), namespace_project_commits_path(project.namespace, project, project.default_branch) · - = link_to pluralize(project.repository.branch_names.count, 'branch'), project_branches_path(project) + = link_to pluralize(project.repository.branch_names.count, 'branch'), namespace_project_branches_path(project.namespace, project) · - = link_to pluralize(project.repository.tag_names.count, 'tag'), project_tags_path(project) + = link_to pluralize(project.repository.tag_names.count, 'tag'), namespace_project_tags_path(project.namespace, project) - else %i.fa.fa-exclamation-triangle Empty repository diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index f797c4e3830..ba2276f51ce 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -1,32 +1,6 @@ +- page_title "Projects" .clearfix - .pull-left - = form_tag explore_projects_path, method: :get, class: 'form-inline form-tiny' do |f| - .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "projects_search" - .form-group - = button_tag 'Search', class: "btn btn-primary wide" - - .pull-right - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light sort: - - if @sort.present? - = @sort.humanize - - else - Name - %b.caret - %ul.dropdown-menu - %li - = link_to explore_projects_path(sort: nil) do - Name - = link_to explore_projects_path(sort: 'newest') do - Newest - = link_to explore_projects_path(sort: 'oldest') do - Oldest - = link_to explore_projects_path(sort: 'recently_updated') do - Recently updated - = link_to explore_projects_path(sort: 'last_updated') do - Last updated + = render 'filter' %hr .public-projects diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index 420f0693756..b5d146b1f2f 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -1,3 +1,4 @@ +- page_title "Starred Projects" .explore-trending-block %p.lead %i.fa.fa-star diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index 9cad9238933..5e24df76a63 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -1,3 +1,10 @@ +- page_title "Trending Projects" +.explore-title + %h3 + Explore GitLab + %p.lead + Discover projects and groups. Share your projects with others +%hr .explore-trending-block %p.lead %i.fa.fa-comments-o @@ -6,6 +13,3 @@ .public-projects %ul.bordered-list = render @trending_projects - - .center - = link_to 'Show all projects', explore_projects_path, class: 'btn btn-primary' diff --git a/app/views/groups/_filter.html.haml b/app/views/groups/_filter.html.haml deleted file mode 100644 index 393be3f1d12..00000000000 --- a/app/views/groups/_filter.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -= form_tag group_filter_path(entity), method: 'get' do - %fieldset - %ul.nav.nav-pills.nav-stacked - %li{class: ("active" if (params[:status] == 'active' || !params[:status]))} - = link_to group_filter_path(entity, status: 'active') do - Active - %li{class: ("active" if params[:status] == 'closed')} - = link_to group_filter_path(entity, status: 'closed') do - Closed - %li{class: ("active" if params[:status] == 'all')} - = link_to group_filter_path(entity, status: 'all') do - All diff --git a/app/views/groups/_new_group_member.html.haml b/app/views/groups/_new_group_member.html.haml deleted file mode 100644 index e590ddbf931..00000000000 --- a/app/views/groups/_new_group_member.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -= form_for @users_group, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| - .form-group - = f.label :user_ids, "People", class: 'control-label' - .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large') - - .form-group - = f.label :access_level, "Group Access", class: 'control-label' - .col-sm-10= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @users_group.access_level), class: "project-access-select select2" - - .form-actions - = f.submit 'Add users into group', class: "btn btn-create" diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 2c65b3049e3..4f8aec1c67e 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -1,21 +1,10 @@ .panel.panel-default - .panel-heading - Projects (#{projects.count}) - - if can? current_user, :create_projects, @group - .panel-head-actions - = link_to new_project_path(namespace_id: @group.id), class: "btn btn-new" do - %i.fa.fa-plus - New project - %ul.well-list - - if projects.blank? - .nothing-here-block This group has no projects yet - - projects.each do |project| - %li.project-row - = link_to project_path(project), class: dom_class(project) do - .dash-project-access-icon - = visibility_level_icon(project.visibility_level) - %span.str-truncated - %span.project-name - = project.name - %span.arrow - %i.fa.fa-angle-right + .panel-heading.clearfix + .input-group + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' + - if can? current_user, :create_projects, @group + %span.input-group-btn + = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-success' do + New project + + = render 'shared/projects_list', projects: @projects, projects_limit: 20 diff --git a/app/views/groups/_settings_nav.html.haml b/app/views/groups/_settings_nav.html.haml deleted file mode 100644 index ec1fb4a2c00..00000000000 --- a/app/views/groups/_settings_nav.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%ul.nav.nav-pills.nav-stacked.nav-stacked-menu - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group) do - %i.fa.fa-pencil-square-o - Group - = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group) do - %i.fa.fa-folder - Projects - diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index eb24fd65d9e..aa13ed85b53 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,41 +1,37 @@ -.row - .col-md-2 - = render 'settings_nav' - .col-md-10 - .panel.panel-default - .panel-heading - %strong= @group.name - group settings: - .panel-body - = form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f| - - if @group.errors.any? - .alert.alert-danger - %span= @group.errors.full_messages.first - = render 'shared/group_form', f: f +- page_title "Settings" +.panel.panel-default + .panel-heading + %strong= @group.name + group settings: + .panel-body + = form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f| + - if @group.errors.any? + .alert.alert-danger + %span= @group.errors.full_messages.first + = render 'shared/group_form', f: f - .form-group - .col-sm-2 - .col-sm-10 - = image_tag group_icon(@group.to_param), alt: '', class: 'avatar s160' - %p.light - - if @group.avatar? - You can change your group avatar here - - else - You can upload a group avatar here - = render 'shared/choose_group_avatar_button', f: f - - if @group.avatar? - %hr - = link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" + .form-group + .col-sm-offset-2.col-sm-10 + = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160' + %p.light + - if @group.avatar? + You can change your group avatar here + - else + You can upload a group avatar here + = render 'shared/choose_group_avatar_button', f: f + - if @group.avatar? + %hr + = link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" - .form-actions - = f.submit 'Save group', class: "btn btn-save" + .form-actions + = f.submit 'Save group', class: "btn btn-save" - .panel.panel-danger - .panel-heading Remove group - .panel-body - %p - Removing group will cause all child projects and resources to be removed. - %br - %strong Removed group can not be restored! +.panel.panel-danger + .panel-heading Remove group + .panel-body + %p + Removing group will cause all child projects and resources to be removed. + %br + %strong Removed group can not be restored! - = link_to 'Remove Group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove" + = link_to 'Remove Group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove" diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index d05016d9c3f..ec39a755f0f 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -1,31 +1,55 @@ - user = member.user -- return unless user +- return unless user || member.invite? - show_roles = true if show_roles.nil? + %li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} %span{class: ("list-item-name" if show_controls)} - = image_tag avatar_icon(user.email, 16), class: "avatar s16" - %strong= user.name - %span.cgray= user.username - - if user == current_user - %span.label.label-success It's you + - if member.user + = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: '' + %strong= user.name + %span.cgray= user.username + - if user == current_user + %span.label.label-success It's you + - if user.blocked? + %label.label.label-danger + %strong Blocked + - else + = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: '' + %strong + = member.invite_email + %span.cgray + invited + - if member.created_by + by + = link_to member.created_by.name, user_path(member.created_by) + = time_ago_with_tooltip(member.created_at) + + - if show_controls && can?(current_user, :admin_group, @group) + = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do + Resend invite - if show_roles %span.pull-right %strong= member.human_access - if show_controls - - if can?(current_user, :modify, member) - = link_to '#', class: "btn-tiny btn js-toggle-button", title: 'Edit access level' do + - if can?(current_user, :modify_group_member, member) + = button_tag class: "btn-xs btn js-toggle-button", + title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o - - if can?(current_user, :destroy, member) - - if current_user == member.user - = link_to leave_profile_group_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do - %i.fa.fa-minus.fa-inverse + - if can?(current_user, :destroy_group_member, member) + + - if current_user == user + = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do + = icon("sign-out") + Leave - else - = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do + = 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 .edit-member.hide.js-toggle-content + %br = form_for [@group, member], remote: true do |f| - .alert.prepend-top-20 - = f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level) - = f.submit 'Save', class: 'btn btn-save btn-small' + .prepend-top-10 + = f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level), {}, class: 'form-control' + .prepend-top-10 + = f.submit 'Save', class: 'btn btn-save btn-sm' diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml new file mode 100644 index 00000000000..3361d7e2a8d --- /dev/null +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -0,0 +1,18 @@ += form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| + .form-group + = f.label :user_ids, "People", class: 'control-label' + .col-sm-10 + = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true) + .help-block + Search for existing users or invite new ones using their email address. + + .form-group + = f.label :access_level, "Group Access", class: 'control-label' + .col-sm-10 + = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "project-access-select select2" + .help-block + Read more about role permissions + %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + + .form-actions + = f.submit 'Add users to group', class: "btn btn-create" diff --git a/app/views/groups/members.html.haml b/app/views/groups/group_members/index.html.haml index d2ebcdab7e1..a70d1ff0697 100644 --- a/app/views/groups/members.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,4 +1,6 @@ +- page_title "Members" - show_roles = should_user_see_group_roles?(current_user, @group) + %h3.page-title Group members - if show_roles @@ -10,14 +12,14 @@ %hr .clearfix.js-toggle-container - = form_tag members_group_path(@group), method: :get, class: 'form-inline member-search-form' do + = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input input-mn-300' } + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' } = button_tag 'Search', class: 'btn' - - if current_user && current_user.can?(:manage_group, @group) + - if current_user && current_user.can?(:admin_group, @group) .pull-right - = link_to '#', class: 'btn btn-new js-toggle-button' do + = button_tag class: 'btn btn-new js-toggle-button', type: 'button' do Add members %i.fa.fa-chevron-down @@ -33,6 +35,7 @@ %ul.well-list - @members.each do |member| = render 'groups/group_members/group_member', member: member, show_roles: show_roles, show_controls: true + = paginate @members, theme: 'gitlab' :coffeescript diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index f2005193f83..66fe7e25871 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -1,24 +1,13 @@ 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(:atom, :private_token => @user.private_token), :rel => "self", :type => "application/atom+xml" - xml.link :href => issues_dashboard_url(:private_token => @user.private_token), :rel => "alternate", :type => "text/html" - xml.id issues_dashboard_url(:private_token => @user.private_token) + 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.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? @issues.each do |issue| - xml.entry do - xml.id project_issue_url(issue.project, issue) - xml.link :href => project_issue_url(issue.project, issue) - xml.title truncate(issue.title, :length => 80) - xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, :width => "40", :height => "40", :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 + issue_to_atom(xml, issue) end end diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 1932ba2f644..e0756e909be 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -1,3 +1,8 @@ +- page_title "Issues" += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues") + %h3.page-title Issues @@ -9,10 +14,12 @@ To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. %hr -.row - .fixed.sidebar-expand-button.hidden-lg.hidden-md - %i.fa.fa-list.fa-2x - .col-md-3.responsive-side - = render 'shared/filter', entity: 'issue' - .col-md-9 - = render 'shared/issues' +.append-bottom-20 + .pull-right + - if current_user + .hidden-xs.pull-left + = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do + %i.fa.fa-rss + + = render 'shared/issuable_filter', type: :issues += render 'shared/issues' diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 86d5acdaa32..3d9e857cc52 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -1,3 +1,4 @@ +- page_title "Merge Requests" %h3.page-title Merge Requests @@ -8,10 +9,6 @@ - if current_user To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. %hr -.row - .fixed.sidebar-expand-button.hidden-lg.hidden-md - %i.fa.fa-list.fa-2x - .col-md-3.responsive-side - = render 'shared/filter', entity: 'merge_request' - .col-md-9 - = render 'shared/merge_requests' +.append-bottom-20 + = render 'shared/issuable_filter', type: :merge_requests += render 'shared/merge_requests' diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml index c95c2e89670..09f9b4b8969 100644 --- a/app/views/groups/milestones/_issue.html.haml +++ b/app/views/groups/milestones/_issue.html.haml @@ -2,9 +2,9 @@ %span.milestone-row - project = issue.project %strong #{project.name} · - = link_to [project, issue] do + = link_to [project.namespace.becomes(Namespace), project, issue] do %span.cgray ##{issue.iid} - = link_to_gfm issue.title, [project, issue], title: issue.title + = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title .pull-right.assignee-icon - if issue.assignee - = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16" + = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16", alt: '' diff --git a/app/views/groups/milestones/_merge_request.html.haml b/app/views/groups/milestones/_merge_request.html.haml index e0c903bfdb2..d0d1426762b 100644 --- a/app/views/groups/milestones/_merge_request.html.haml +++ b/app/views/groups/milestones/_merge_request.html.haml @@ -2,9 +2,9 @@ %span.milestone-row - project = merge_request.project %strong #{project.name} · - = link_to [project, merge_request] do + = link_to [project.namespace.becomes(Namespace), project, merge_request] do %span.cgray ##{merge_request.iid} - = link_to_gfm merge_request.title, [project, merge_request], title: merge_request.title + = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title .pull-right.assignee-icon - if merge_request.assignee - = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16" + = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16", alt: '' diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml new file mode 100644 index 00000000000..ba30e6e07c6 --- /dev/null +++ b/app/views/groups/milestones/_milestone.html.haml @@ -0,0 +1,25 @@ +%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } + .pull-right + - if can?(current_user, :admin_group, @group) + - if milestone.closed? + = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" + - else + = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" + %h4 + = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title) + .row + .col-sm-6 + = link_to issues_group_path(@group, milestone_title: milestone.title) do + = pluralize milestone.issue_count, 'Issue' + + = link_to merge_requests_group_path(@group, milestone_title: milestone.title) do + = pluralize milestone.merge_requests_count, 'Merge Request' + + %span.light #{milestone.percent_complete}% complete + .col-sm-6 + = milestone_progress_bar(milestone) + %div + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label.label-gray + = milestone.project.name diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 2727525f070..385222fa5b7 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Milestones" %h3.page-title Milestones %span.pull-right #{@group_milestones.count} milestones @@ -9,42 +10,14 @@ %hr -.row - .fixed.sidebar-expand-button.hidden-lg.hidden-md - %i.fa.fa-list.fa-2x - .col-md-3.responsive-side - = render 'groups/filter', entity: 'milestone' - .col-md-9 - .panel.panel-default - %ul.well-list - - if @group_milestones.blank? - %li - .nothing-here-block No milestones to show - - else - - @group_milestones.each do |milestone| - %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } - .pull-right - - if can?(current_user, :manage_group, @group) - - if milestone.closed? - = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-small btn-grouped btn-reopen" - - else - = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-small btn-close" - %h4 - = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title) - %div - %div - = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do - = pluralize milestone.issue_count, 'Issue' - - = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do - = pluralize milestone.merge_requests_count, 'Merge Request' - - %span.light #{milestone.percent_complete}% complete - .progress.progress-info - .progress-bar{style: "width: #{milestone.percent_complete}%;"} - %div - %br - - milestone.projects.each do |project| - %span.label.label-default - = project.name - = paginate @group_milestones, theme: "gitlab" += render 'shared/milestones_filter' +.milestones + .panel.panel-default + %ul.well-list + - if @group_milestones.blank? + %li + .nothing-here-block No milestones to show + - else + - @group_milestones.each do |milestone| + = render 'milestone', milestone: milestone + = paginate @group_milestones, theme: "gitlab" diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 411d1822be0..8f2decb851f 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -1,52 +1,52 @@ -%h3.page-title +- page_title @group_milestone.title, "Milestones" +%h4.page-title + .issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" } + - if @group_milestone.closed? + Closed + - else + Open Milestone #{@group_milestone.title} .pull-right - - if can?(current_user, :manage_group, @group) + - if can?(current_user, :admin_group, @group) - if @group_milestone.active? - = link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-small btn-close" + = link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" - else - = link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-small btn-grouped btn-reopen" + = link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" +%hr - if (@group_milestone.total_items_count == @group_milestone.closed_items_count) && @group_milestone.active? .alert.alert-success %span All issues for this milestone are closed. You may close the milestone now. -.back-link - = link_to group_milestones_path(@group) do - ← To milestones list - -.issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" } - .state.clearfix - .state-label - - if @group_milestone.closed? - Closed - - else - Open - - %h4.title - = gfm escape_once(@group_milestone.title) - - .description - - @group_milestone.milestones.each do |milestone| - %hr - %h4 - = link_to "#{milestone.project.name} - #{milestone.title}", project_milestone_path(milestone.project, milestone) - %span.pull-right= milestone.expires_at +.description +%table.table + %thead + %tr + %th Project + %th Open issues + %th State + %th Due date + - @group_milestone.milestones.each do |milestone| + %tr + %td + = link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) + %td + = milestone.issues.opened.count + %td - if milestone.closed? - %span.label.label-danger #{milestone.state} - = preserve do - - if milestone.description.present? - = milestone.description + Closed + - else + Open + %td + = milestone.expires_at - .context - %p - Progress: - #{@group_milestone.closed_items_count} closed - – - #{@group_milestone.open_items_count} open - - .progress.progress-info - .progress-bar{style: "width: #{@group_milestone.percent_complete}%;"} +.context + %p.lead + Progress: + #{@group_milestone.closed_items_count} closed + – + #{@group_milestone.open_items_count} open + = milestone_progress_bar(@group_milestone) %ul.nav.nav-tabs %li.active @@ -62,6 +62,9 @@ Participants %span.badge= @group_milestone.participants.count + .pull-right + = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @group_milestone.title), class: "btn edit-milestone-link btn-grouped" + .tab-content .tab-pane.active#tab-issues .row diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 6e17cdaef6f..0665cdf387a 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -1,3 +1,5 @@ +- page_title 'New Group' +- header_title 'New Group' = form_for @group, html: { class: 'group-form form-horizontal' } do |f| - if @group.errors.any? .alert.alert-danger @@ -11,8 +13,7 @@ = render 'shared/choose_group_avatar_button', f: f .form-group - .col-sm-2 - .col-sm-10 + .col-sm-offset-2.col-sm-10 = render 'shared/group_tips' .form-actions diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 65a66355c56..6b7efa83dea 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,29 +1,26 @@ -.row - .col-md-2 - = render 'settings_nav' - .col-md-10 - .panel.panel-default - .panel-heading - %strong= @group.name - projects: - - if can? current_user, :manage_group, @group - .panel-head-actions - = link_to new_project_path(namespace_id: @group.id), class: "btn btn-new" do - %i.fa.fa-plus - New Project - %ul.well-list - - @projects.each do |project| - %li - .list-item-name - = visibility_level_icon(project.visibility_level) - %strong= link_to project.name_with_namespace, project - %span.label.label-gray - = repository_size(project) - .pull-right - = link_to 'Members', project_team_index_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" - = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" - = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-small btn-remove" - - if @projects.blank? - .nothing-here-block This group has no projects yet +- page_title "Projects" +.panel.panel-default + .panel-heading + %strong= @group.name + projects: + - if can? current_user, :admin_group, @group + .panel-head-actions + = link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do + %i.fa.fa-plus + New Project + %ul.well-list + - @projects.each do |project| + %li + .list-item-name + = visibility_level_icon(project.visibility_level) + %strong= link_to project.name_with_namespace, project + %span.label.label-gray + = repository_size(project) + .pull-right + = link_to 'Members', namespace_project_project_members_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" + = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" + = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove" + - if @projects.blank? + .nothing-here-block This group has no projects yet - = paginate @projects, theme: "gitlab" += paginate @projects, theme: "gitlab" diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index e07bb7d2fb7..a91d1a6e94b 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -1,28 +1,12 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "Group feed - #{@group.name}" - xml.link :href => group_path(@group, :atom), :rel => "self", :type => "application/atom+xml" - xml.link :href => group_path(@group), :rel => "alternate", :type => "text/html" - xml.id projects_url + xml.title "#{@group.name} activity" + xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: group_url(@group), rel: "alternate", type: "text/html" + xml.id group_url(@group) xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| - if event.proper? - xml.entry do - event_link = event_feed_url(event) - event_title = event_feed_title(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.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, :width => "40", :height => "40", :url => avatar_icon(event.author_email) - xml.author do |author| - xml.name event.author_name - xml.email event.author_email - end - xml.summary event_title - end - end + event_to_atom(xml, event) end end diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index d876e87852c..d31dae7d648 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,37 +1,37 @@ += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") + .dashboard - %section.activities.col-md-8.hidden-sm.hidden-xs - - if current_user - = render "events/event_last_push", event: @last_push - = link_to dashboard_path, class: 'btn btn-tiny' do - ← To dashboard - - %span.cgray - Currently you are only seeing events from the + .header-with-avatar.clearfix + = image_tag group_icon(@group), class: "avatar group-avatar s90" + %h3 = @group.name - group - %hr - = render 'shared/event_filter' - - if @events.any? - .content_list - - else - .nothing-here-block Project activity will be displayed here - = spinner - %aside.side.col-md-4 - .light-well.append-bottom-20 - = image_tag group_icon(@group.path), class: "avatar s90" - .clearfix.light - %h3.page-title - = @group.name - - if @group.description.present? - %p - = escaped_autolink(@group.description) - = render "projects", projects: @projects - - if current_user - .prepend-top-20 - = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed" do - %strong - %i.fa.fa-rss - News Feed + .username + @#{@group.path} + - if @group.description.present? + .description + = markdown(@group.description, pipeline: :description) + %hr + + = render 'shared/show_aside' + + .row + %section.activities.col-md-8 + .hidden-xs + - if current_user + = render "events/event_last_push", event: @last_push - %hr - = render 'shared/promo' + - if current_user + %ul.nav.nav-pills.event_filter.pull-right + %li + = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'rss-btn' do + %i.fa.fa-rss + + = render 'shared/event_filter' + %hr + + .content_list + = spinner + %aside.side.col-md-4 + = render "projects", projects: @projects diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 7b21ca30d8c..825acb0ae3e 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -1,4 +1,4 @@ -#modal-shortcuts.modal.hide{tabindex: -1} +#modal-shortcuts.modal{tabindex: -1} .modal-dialog .modal-content .modal-header @@ -187,7 +187,11 @@ %td.shortcut .key m %td Change milestone - %tbody{ class: 'hidden-shortcut merge_reuests', style: 'display:none' } + %tr + %td.shortcut + .key r + %td Reply (quoting selected text) + %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' } %tr %th %th Merge Requests @@ -199,6 +203,10 @@ %td.shortcut .key m %td Change milestone + %tr + %td.shortcut + .key r + %td Reply (quoting selected text) :javascript diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 7b8193abfdf..bf4b7234b21 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -3,6 +3,8 @@ GitLab %span= Gitlab::VERSION %small= Gitlab::REVISION + - if current_application_settings.version_check_enabled + = version_status_badge %p.slead GitLab is open source software to collaborate on code. %br @@ -42,3 +44,9 @@ %li Use = link_to 'shortcuts', '#', onclick: 'Shortcuts.showHelp(event)' + %li + Get a support + = link_to 'subscription', 'https://about.gitlab.com/pricing/' + %li + = link_to 'Compare', 'https://about.gitlab.com/features/#compare' + GitLab editions diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml index 67f9cc41cf3..8551496b98a 100644 --- a/app/views/help/show.html.haml +++ b/app/views/help/show.html.haml @@ -1,2 +1,3 @@ +- page_title @file.humanize, *@category.split("/").reverse.map(&:humanize) .documentation.wiki - = markdown File.read(Rails.root.join('doc', @category, @file + '.md')) + = markdown @markdown.gsub('$your_email', current_user.email) diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml new file mode 100644 index 00000000000..7c89457ace3 --- /dev/null +++ b/app/views/help/ui.html.haml @@ -0,0 +1,228 @@ +- page_title "UI Development Kit", "Help" +- lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum nisi sapien, non consequat lectus aliquam ultrices. Suspendisse sodales est euismod nunc condimentum, a consectetur diam ornare." + +.gitlab-ui-dev-kit + %h1 GitLab UI development kit + %p.light + Use page inspector in your browser to check element classes and structure + of examples below. + %hr + %ul + %li + = link_to 'Blocks', '#blocks' + %li + = link_to 'Lists', '#lists' + %li + = link_to 'Tables', '#tables' + %li + = link_to 'Buttons', '#buttons' + %li + = link_to 'Panels', '#panels' + %li + = link_to 'Alerts', '#alerts' + %li + = link_to 'Forms', '#forms' + %li + = link_to 'Files', '#file' + %li + = link_to 'Markdown', '#markdown' + + %h2#blocks Blocks + + %h3 + %code .well + + + .well + %h4 Something + = lorem + + + %h2#lists Lists + + %h3 + %code .well-list + %ul.well-list + %li + One item + %li + One item + %li + One item + + %h3 + %code .panel .well-list + + .panel.panel-default + .panel-heading Your list + %ul.well-list + %li + One item + %li + One item + %li + One item + + %h3 + %code .bordered-list + %ul.bordered-list + %li + One item + %li + One item + %li + One item + + + + %h2#tables Tables + + .example + %table.table + %thead + %tr + %th # + %th First Name + %th Last Name + %th Username + %tbody + %tr + %td 1 + %td Mark + %td Otto + %td @mdo + %tr + %td 2 + %td Jacob + %td Thornton + %td @fat + %tr + %td 3 + %td Larry + %td the Bird + %td @twitter + + + %h2#buttons Buttons + + .example + %button.btn.btn-default{:type => "button"} Default + %button.btn.btn-primary{:type => "button"} Primary + %button.btn.btn-success{:type => "button"} Success + %button.btn.btn-info{:type => "button"} Info + %button.btn.btn-warning{:type => "button"} Warning + %button.btn.btn-danger{:type => "button"} Danger + %button.btn.btn-link{:type => "button"} Link + + %h2#panels Panels + + .row + .col-md-6 + .panel.panel-success + .panel-heading Success + .panel-body + = lorem + .panel.panel-primary + .panel-heading Primary + .panel-body + = lorem + .panel.panel-info + .panel-heading Info + .panel-body + = lorem + .col-md-6 + .panel.panel-warning + .panel-heading Warning + .panel-body + = lorem + .panel.panel-danger + .panel-heading Danger + .panel-body + = lorem + + %h2#alert Alerts + + .row + .col-md-6 + .alert.alert-success + = lorem + .alert.alert-primary + = lorem + .alert.alert-info + = lorem + .col-md-6 + .alert.alert-warning + = lorem + .alert.alert-danger + = lorem + + %h2#forms Forms + + %h3 + %code form.horizontal-form + + %form.form-horizontal + .form-group + %label.col-sm-2.control-label{:for => "inputEmail3"} Email + .col-sm-10 + %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/ + .form-group + %label.col-sm-2.control-label{:for => "inputPassword3"} Password + .col-sm-10 + %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/ + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + %label + %input{:type => "checkbox"}/ + Remember me + .form-group + .col-sm-offset-2.col-sm-10 + %button.btn.btn-default{:type => "submit"} Sign in + + %h3 + %code form + + %form + .form-group + %label{:for => "exampleInputEmail1"} Email address + %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/ + .form-group + %label{:for => "exampleInputPassword1"} Password + %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/ + .checkbox + %label + %input{:type => "checkbox"}/ + Remember me + %button.btn.btn-default{:type => "submit"} Sign in + + %h2#file File + %h3 + %code .file-holder + + - blob = Snippet.new(content: "Wow\nSuch\nFile") + .example + .file-holder + .file-title + Awesome file + .file-actions + .btn-group + %a.btn Edit + %a.btn Remove + .file-contenta.code + = render 'shared/file_highlight', blob: blob + + + %h2#markdown Markdown + %h3 + %code .md or .wiki and others + + Markdown rendering has a bit different css and presented in next UI elements: + + %ul + %li comment + %li issue, merge request description + %li wiki page + %li help page + + You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown", "markdown")}. diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml new file mode 100644 index 00000000000..90a6f5f9d2d --- /dev/null +++ b/app/views/import/base/create.js.haml @@ -0,0 +1,25 @@ +- if @already_been_taken + :plain + target_field = $("tr#repo_#{@repo_id} .import-target") + origin_target = target_field.text() + project_name = "#{@project_name}" + origin_namespace = "#{@target_namespace}" + target_field.empty() + target_field.append("<p class='alert alert-danger'>This namespace already been taken! Please choose another one</p>") + target_field.append("<input type='text' name='target_namespace' />") + target_field.append("/" + project_name) + target_field.data("project_name", project_name) + target_field.find('input').prop("value", origin_namespace) +- elsif @access_denied + :plain + job = $("tr#repo_#{@repo_id}") + job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>") +- else + :plain + job = $("tr#repo_#{@repo_id}") + job.attr("id", "project_#{@project.id}") + target_field = job.find(".import-target") + target_field.empty() + target_field.append('<strong>#{link_to @project.path_with_namespace, [@project.namespace.becomes(Namespace), @project]}</strong>') + $("table.import-jobs tbody").prepend(job) + job.addClass("active").find(".import-actions").html("<i class='fa fa-spinner fa-spin'></i> started") diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml new file mode 100644 index 00000000000..9d2858e4e72 --- /dev/null +++ b/app/views/import/bitbucket/status.html.haml @@ -0,0 +1,46 @@ +- page_title "Bitbucket import" +%h3.page-title + %i.fa.fa-bitbucket + Import projects from Bitbucket + +%p.light + Select projects you want to import. +%hr +%p + = button_tag 'Import all projects', class: "btn btn-success js-import-all" + +%table.table.import-jobs + %thead + %tr + %th From Bitbucket + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank" + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} + %td + = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" + %td.import-target + = "#{repo["owner"]}/#{repo["slug"]}" + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" + +:coffeescript + new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}") diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml new file mode 100644 index 00000000000..ef552498239 --- /dev/null +++ b/app/views/import/github/status.html.haml @@ -0,0 +1,46 @@ +- page_title "GitHub import" +%h3.page-title + %i.fa.fa-github + Import projects from GitHub + +%p.light + Select projects you want to import. +%hr +%p + = button_tag 'Import all projects', class: "btn btn-success js-import-all" + +%table.table.import-jobs + %thead + %tr + %th From GitHub + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank" + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = link_to repo.full_name, "https://github.com/#{repo.full_name}", target: "_blank" + %td.import-target + = repo.full_name + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" + +:coffeescript + new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}") diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml new file mode 100644 index 00000000000..727f3c7e7fa --- /dev/null +++ b/app/views/import/gitlab/status.html.haml @@ -0,0 +1,46 @@ +- page_title "GitLab.com import" +%h3.page-title + %i.fa.fa-heart + Import projects from GitLab.com + +%p.light + Select projects you want to import. +%hr +%p + = button_tag 'Import all projects', class: "btn btn-success js-import-all" + +%table.table.import-jobs + %thead + %tr + %th From GitLab.com + %th To this GitLab instance + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo["id"]}"} + %td + = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank" + %td.import-target + = repo["path_with_namespace"] + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" + +:coffeescript + new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}") diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml new file mode 100644 index 00000000000..bff7ee7c85d --- /dev/null +++ b/app/views/import/gitorious/status.html.haml @@ -0,0 +1,46 @@ +- page_title "Gitorious import" +%h3.page-title + %i.icon-gitorious.icon-gitorious-big + Import projects from Gitorious.org + +%p.light + Select projects you want to import. +%hr +%p + = button_tag 'Import all projects', class: "btn btn-success js-import-all" + +%table.table.import-jobs + %thead + %tr + %th From Gitorious.org + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank" + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = link_to repo.full_name, "https://gitorious.org/#{repo.full_name}", target: "_blank" + %td.import-target + = repo.full_name + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" + +:coffeescript + new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}") diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml new file mode 100644 index 00000000000..9c64e0a009f --- /dev/null +++ b/app/views/import/google_code/new.html.haml @@ -0,0 +1,61 @@ +- page_title "Google Code import" +%h3.page-title + %i.fa.fa-google + Import projects from Google Code +%hr + += form_tag callback_import_google_code_path, class: 'form-horizontal', multipart: true do + %p + Follow the steps below to export your Google Code project data. + In the next step, you'll be able to select the projects you want to import. + %ol + %li + %p + Go to + #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: "_blank"}. + %li + %p + Make sure you're logged into the account that owns the projects you'd like to import. + %li + %p + Click the <strong>Select none</strong> button on the right, since we only need "Google Code Project Hosting". + %li + %p + Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right. + %li + %p + Choose <strong>Next</strong> at the bottom of the page. + %li + %p + Leave the "File type" and "Delivery method" options on their default values. + %li + %p + Choose <strong>Create archive</strong> and wait for archiving to complete. + %li + %p + Click the <strong>Download</strong> button and wait for downloading to complete. + %li + %p + Find the downloaded ZIP file and decompress it. + %li + %p + Find the newly extracted <code>Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json</code> file. + %li + %p + Upload <code>GoogleCodeProjectHosting.json</code> here: + %p + %input{type: "file", name: "dump_file", id: "dump_file"} + %li + %p + Do you want to customize how Google Code email addresses and usernames are imported into GitLab? + %p + = label_tag :create_user_map_0 do + = radio_button_tag :create_user_map, 0, true + No, directly import the existing email addresses and usernames. + %p + = label_tag :create_user_map_1 do + = radio_button_tag :create_user_map, 1, false + Yes, let me map Google Code users to full names or GitLab users. + %li + %p + = submit_tag 'Continue to the next step', class: "btn btn-create" diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml new file mode 100644 index 00000000000..e53ebda7dc1 --- /dev/null +++ b/app/views/import/google_code/new_user_map.html.haml @@ -0,0 +1,43 @@ +- page_title "User map", "Google Code import" +%h3.page-title + %i.fa.fa-google + Import projects from Google Code +%hr + += form_tag create_user_map_import_google_code_path, class: 'form-horizontal' do + %p + Customize how Google Code email addresses and usernames are imported into GitLab. + In the next step, you'll be able to select the projects you want to import. + %p + The user map is a JSON document mapping the Google Code users that participated on your projects to the way their email addresses and usernames will be imported into GitLab. You can change this by changing the value on the right hand side of <code>:</code>. Be sure to preserve the surrounding double quotes, other punctuation and the email address or username on the left hand side. + %ul + %li + %strong Default: Directly import the Google Code email address or username + %p + <code>"johnsmith@example.com": "johnsm...@example.com"</code> + will add "By johnsm...@example.com" to all issues and comments originally created by johnsmith@example.com. + The email address or username is masked to ensure the user's privacy. + %li + %strong Map a Google Code user to a GitLab user + %p + <code>"johnsmith@example.com": "@johnsmith"</code> + will add "By <a href="#">@johnsmith</a>" to all issues and comments originally created by johnsmith@example.com, + and will set <a href="#">@johnsmith</a> as the assignee on all issues originally assigned to johnsmith@example.com. + %li + %strong Map a Google Code user to a full name + %p + <code>"johnsmith@example.com": "John Smith"</code> + will add "By John Smith" to all issues and comments originally created by johnsmith@example.com. + %li + %strong Map a Google Code user to a full email address + %p + <code>"johnsmith@example.com": "johnsmith@example.com"</code> + will add "By <a href="#">johnsmith@example.com</a>" to all issues and comments originally created by johnsmith@example.com. + By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address. + + .form-group + .col-sm-12 + = text_area_tag :user_map, JSON.pretty_generate(@user_map), class: 'form-control', rows: 15 + + .form-actions + = submit_tag 'Continue to the next step', class: "btn btn-create" diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml new file mode 100644 index 00000000000..e8ec79e72f7 --- /dev/null +++ b/app/views/import/google_code/status.html.haml @@ -0,0 +1,70 @@ +- page_title "Google Code import" +%h3.page-title + %i.fa.fa-google + Import projects from Google Code + +- if @repos.any? + %p.light + Select projects you want to import. + %p.light + Optionally, you can + = link_to "customize", new_user_map_import_google_code_path + how Google Code email addresses and usernames are imported into GitLab. + %hr + %p + - if @incompatible_repos.any? + = button_tag 'Import all compatible projects', class: "btn btn-success js-import-all" + - else + = button_tag 'Import all projects', class: "btn btn-success js-import-all" + +%table.table.import-jobs + %thead + %tr + %th From Google Code + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank" + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" + %td.import-target + = "#{current_user.username}/#{repo.name}" + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" + - @incompatible_repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" + %td.import-target + %td.import-actions-job-status + = label_tag "Incompatible Project", nil, class: "label label-danger" + +- if @incompatible_repos.any? + %p + One or more of your Google Code projects cannot be imported into GitLab + directly because they use Subversion or Mercurial for version control, + rather than Git. Please convert them to Git on Google Code, and go + through the + = link_to "import flow", new_import_google_code_path + again. + +:coffeescript + new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}") diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml new file mode 100644 index 00000000000..2fd4859c1c6 --- /dev/null +++ b/app/views/invites/show.html.haml @@ -0,0 +1,30 @@ +- page_title "Invitation" +%h3.page-title Invitation + +%p + You have been invited + - if inviter = @member.created_by + by + = link_to inviter.name, user_url(inviter) + to join + - case @member.source + - when Project + - project = @member.source + project + %strong + = link_to project.name_with_namespace, namespace_project_url(project.namespace, project) + - when Group + - group = @member.source + group + %strong + = link_to group.name, group_url(group) + as #{@member.human_access}. + +- if @member.source.users.include?(current_user) + %p + However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}. + Sign in using a different account to accept the invitation. +- else + .actions + = link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success" + = link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10" diff --git a/app/views/layouts/_bootlint.haml b/app/views/layouts/_bootlint.haml new file mode 100644 index 00000000000..69280687a9d --- /dev/null +++ b/app/views/layouts/_bootlint.haml @@ -0,0 +1,4 @@ +:javascript + jQuery(document).ready(function() { + javascript:(function(){var s=document.createElement("script");s.onload=function(){bootlint.showLintReportForCurrentDocument([], {hasProblems: false, problemFree: false});};s.src="https://maxcdn.bootstrapcdn.com/bootlint/latest/bootlint.min.js";document.body.appendChild(s)})(); + }); diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml new file mode 100644 index 00000000000..2ed51d87ca1 --- /dev/null +++ b/app/views/layouts/_collapse_button.html.haml @@ -0,0 +1,4 @@ +- 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" diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index fa6aecb6661..dbc68c39bf1 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -1,34 +1,22 @@ +- page_title "GitLab" %head %meta{charset: "utf-8"} - - -# Go repository retrieval support - -# Need to be the fist thing in the head - -# Since Go is using an XML parser to process HTML5 - -# https://github.com/gitlabhq/gitlabhq/pull/5958#issuecomment-45397555 - - if controller_name == 'projects' && action_name == 'show' - %meta{name: "go-import", content: "#{@project.web_url_without_protocol} git #{@project.web_url}.git"} + %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'} %meta{content: "GitLab Community Edition", name: "description"} - %title - = "#{title} | " if defined?(title) - GitLab + %title= page_title + = favicon_link_tag 'favicon.ico' = stylesheet_link_tag "application", :media => "all" = stylesheet_link_tag "print", :media => "print" = javascript_include_tag "application" = csrf_meta_tags = include_gon - %meta{name: 'viewport', content: 'width=device-width, initial-scale=1.0'} + %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'} + %meta{name: 'theme-color', content: '#474D57'} + + = yield :meta_tags = render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id') = render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id') - - -# Atom feed - - if current_user - - if controller_name == 'projects' && action_name == 'index' - = auto_discovery_link_tag :atom, projects_url(:atom, private_token: current_user.private_token), title: "Dashboard feed" - - if @project && !@project.new_record? - - if current_controller?(:tree, :commits) - = auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, format: :atom, private_token: current_user.private_token), title: "Recent commits to #{@project.name}:#{@ref}") - - if current_controller?(:issues) - = auto_discovery_link_tag(:atom, project_issues_url(@project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") + = render 'layouts/bootlint' if Rails.env.development? diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml deleted file mode 100644 index 5dcaee2fa02..00000000000 --- a/app/views/layouts/_head_panel.html.haml +++ /dev/null @@ -1,48 +0,0 @@ -%header.navbar.navbar-static-top.navbar-gitlab - .navbar-inner - .container - %div.app_logo - %span.separator - = link_to root_path, class: "home has_bottom_tooltip", title: "Dashboard" do - %h1 GITLAB - %span.separator - %h1.title= title - - %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"} - %span.sr-only Toggle navigation - %i.fa.fa-bars - - .navbar-collapse.collapse - %ul.nav.navbar-nav - %li.hidden-sm.hidden-xs - = render "layouts/search" - %li.visible-sm.visible-xs - = link_to search_path, title: "Search", class: 'has_bottom_tooltip', 'data-original-title' => 'Search area' do - %i.fa.fa-search - %li - = link_to help_path, title: 'Help', class: 'has_bottom_tooltip', - 'data-original-title' => 'Help' do - %i.fa.fa-question-circle - %li - = link_to explore_root_path, title: "Explore", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do - %i.fa.fa-globe - %li - = link_to user_snippets_path(current_user), title: "My snippets", class: 'has_bottom_tooltip', 'data-original-title' => 'My snippets' do - %i.fa.fa-clipboard - - if current_user.is_admin? - %li - = link_to admin_root_path, title: "Admin area", class: 'has_bottom_tooltip', 'data-original-title' => 'Admin area' do - %i.fa.fa-cogs - - if current_user.can_create_project? - %li - = link_to new_project_path, title: "New project", class: 'has_bottom_tooltip', 'data-original-title' => 'New project' do - %i.fa.fa-plus - %li - = link_to profile_path, title: "Profile settings", class: 'has_bottom_tooltip', 'data-original-title' => 'Profile settings"' do - %i.fa.fa-user - %li - = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Logout", class: 'has_bottom_tooltip', 'data-original-title' => 'Logout' do - %i.fa.fa-sign-out - %li.hidden-xs - = link_to current_user, class: "profile-pic", id: 'profile-pic' do - = image_tag avatar_icon(current_user.email, 26), alt: 'User activity' diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index 353f7ce34f1..3c58f10e759 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -1,3 +1,3 @@ :javascript - GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_project_path(@project, type: @noteable.class, type_id: params[:id])}" + GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(@project.namespace, @project, type: @noteable.class, type_id: params[:id])}" GitLab.GfmAutoComplete.setup(); diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml new file mode 100644 index 00000000000..f17f6fdd91c --- /dev/null +++ b/app/views/layouts/_page.html.haml @@ -0,0 +1,20 @@ +.page-with-sidebar{ class: nav_sidebar_class } + = render "layouts/broadcast" + .sidebar-wrapper + - if defined?(sidebar) && sidebar + = render "layouts/nav/#{sidebar}" + - elsif current_user + = render 'layouts/nav/dashboard' + .collapse-nav + = render partial: 'layouts/collapse_button' + - if current_user + = link_to current_user, class: 'sidebar-user' do + = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s32' + .username + = current_user.username + .content-wrapper + .container-fluid + .content + = render "layouts/flash" + .clearfix + = yield diff --git a/app/views/layouts/_public_head_panel.html.haml b/app/views/layouts/_public_head_panel.html.haml deleted file mode 100644 index 9bfc14d16c1..00000000000 --- a/app/views/layouts/_public_head_panel.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -%header.navbar.navbar-static-top.navbar-gitlab - .navbar-inner - .container - %div.app_logo - %span.separator - = link_to explore_root_path, class: "home" do - %h1 GITLAB - %span.separator - %h1.title= title - - %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"} - %span.sr-only Toggle navigation - %i.fa.fa-bars - - .pull-right.hidden-xs - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-new' - - .navbar-collapse.collapse - %ul.nav.navbar-nav - %li.visible-xs - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes') - diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 04f79846858..e2d2dec7ab8 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,6 +1,6 @@ .search = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input" + = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input form-control" = hidden_field_tag :group_id, @group.try(:id) - if @project && @project.persisted? = hidden_field_tag :project_id, @project.id diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 207ab22f4c7..1c738719bd8 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,13 +1,5 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: "Admin area" - %body{class: "#{app_theme} admin", :'data-page' => body_data_page} - = render "layouts/broadcast" - = render "layouts/head_panel", title: "Admin area" - %nav.main-nav.navbar-collapse.collapse - .container= render 'layouts/nav/admin' - .container - .content - = render "layouts/flash" - = yield - = yield :embedded_scripts +- page_title "Admin area" +- header_title "Admin area", admin_root_path +- sidebar "admin" + += render template: "layouts/application" diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 7d0819aa93e..678ed3c2c1f 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,12 +1,15 @@ !!! 5 %html{ lang: "en"} - = render "layouts/head", title: "Dashboard" - %body{class: "#{app_theme} application", :'data-page' => body_data_page } - = render "layouts/broadcast" - = render "layouts/head_panel", title: "Dashboard" - %nav.main-nav.navbar-collapse.collapse - .container= render 'layouts/nav/dashboard' - .container - .content - = render "layouts/flash" - = yield + = render "layouts/head" + %body{class: "#{user_application_theme}", 'data-page' => body_data_page} + -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. + = yield :scripts_body_top + + - if current_user + = render "layouts/header/default", title: header_title + - else + = render "layouts/header/public", title: header_title + + = render 'layouts/page', sidebar: sidebar + + = yield :scripts_body diff --git a/app/views/layouts/dashboard.html.haml b/app/views/layouts/dashboard.html.haml new file mode 100644 index 00000000000..c72eca10bf4 --- /dev/null +++ b/app/views/layouts/dashboard.html.haml @@ -0,0 +1,5 @@ +- page_title "Dashboard" +- header_title "Dashboard", root_path +- sidebar "dashboard" + += render template: "layouts/application" diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 06de03eadad..1987bf1592a 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -1,35 +1,32 @@ !!! 5 %html{ lang: "en"} = render "layouts/head" - %body.ui_basic.login-page - .container - .content - .login-title - %h1= brand_title - %hr - .container + %body.ui_charcoal.login-page.application + = render "layouts/header/empty" + = render "layouts/broadcast" + .container.navless-container .content = render "layouts/flash" - .row - .col-md-7.brand-holder + .row.prepend-top-20 + .col-sm-5.pull-right + = yield + .col-sm-7.brand-holder.pull-left + %h1 + = brand_title - if brand_item - .brand-image - = brand_image - .brand_text - = brand_text + = brand_image + = brand_text - else - .brand-image.default-brand-image.hidden-sm.hidden-xs - = image_tag 'brand_logo.png' - .brand_text.hidden-xs - %h2 Open source software to collaborate on code + %h3 Open source software to collaborate on code - %p.lead - Manage git repositories with fine grained access controls that keep your code secure. - Perform code reviews and enhance collaboration with merge requests. - Each project can also have an issue tracker and a wiki. + %p + Manage git repositories with fine grained access controls that keep your code secure. + Perform code reviews and enhance collaboration with merge requests. + Each project can also have an issue tracker and a wiki. + + - if extra_sign_in_text.present? + = markdown(extra_sign_in_text) - .col-md-5 - = yield %hr .container .footer-links diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 16df9c10fbb..2af265a2296 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -1,8 +1,8 @@ !!! 5 %html{ lang: "en"} - = render "layouts/head", title: "Error" - %body{class: "#{app_theme} application"} - = render "layouts/head_panel", title: "" if current_user + = render "layouts/head" + %body{class: "#{user_application_theme} application"} + = render "layouts/header/empty" .container.navless-container = render "layouts/flash" .error-page diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml index d023846c5eb..56bb92a536e 100644 --- a/app/views/layouts/explore.html.haml +++ b/app/views/layouts/explore.html.haml @@ -1,30 +1,5 @@ -- page_title = 'Explore' -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: page_title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/broadcast" - - if current_user - = render "layouts/head_panel", title: page_title - - else - = render "layouts/public_head_panel", title: page_title - .container.navless-container - .content - .explore-title - %h3 - Explore GitLab - %p.lead - Discover projects and groups. Share your projects with others +- page_title "Explore" +- header_title "Explore GitLab", explore_root_path +- sidebar "explore" - - %ul.nav.nav-tabs - = nav_link(path: 'projects#trending') do - = link_to 'Trending Projects', explore_root_path - = nav_link(path: 'projects#starred') do - = link_to 'Most Starred Projects', starred_explore_projects_path - = nav_link(path: 'projects#index') do - = link_to 'All Projects', explore_projects_path - = nav_link(controller: :groups) do - = link_to 'All Groups', explore_groups_path - - = yield += render template: "layouts/application" diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index f22fb236cb5..5edc03129d2 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,12 +1,5 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: group_head_title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/broadcast" - = render "layouts/head_panel", title: "group: #{@group.name}" - %nav.main-nav.navbar-collapse.collapse - .container= render 'layouts/nav/group' - .container - .content - = render "layouts/flash" - = yield +- page_title @group.name +- header_title @group.name, group_path(@group) +- sidebar "group" + += render template: "layouts/application" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml new file mode 100644 index 00000000000..1403b86f377 --- /dev/null +++ b/app/views/layouts/header/_default.html.haml @@ -0,0 +1,45 @@ +%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } + .container + .header-logo + = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do + = brand_header_logo + %h3 GitLab + .header-content + %button.navbar-toggle{type: 'button'} + %span.sr-only Toggle navigation + = icon('bars') + + .navbar-collapse.collapse + %ul.nav.navbar-nav.pull-right + %li.hidden-sm.hidden-xs + = render 'layouts/search' + %li.visible-sm.visible-xs + = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('search') + %li.hidden-xs + = link_to help_path, title: 'Help', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('question-circle fw') + %li + = link_to explore_root_path, title: 'Explore', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('globe fw') + %li + = link_to user_snippets_path(current_user), title: 'Your snippets', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('clipboard fw') + - if current_user.is_admin? + %li + = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('wrench fw') + - if current_user.can_create_project? + %li.hidden-xs + = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('plus fw') + %li + = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('cog fw') + %li + = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('sign-out') + + %h1.title= title + += render 'shared/outdated_browser' diff --git a/app/views/layouts/header/_empty.html.haml b/app/views/layouts/header/_empty.html.haml new file mode 100644 index 00000000000..2ed4edb1136 --- /dev/null +++ b/app/views/layouts/header/_empty.html.haml @@ -0,0 +1,4 @@ +%header.navbar.navbar-fixed-top.navbar-empty + .container + .center-logo + = brand_header_logo diff --git a/app/views/layouts/header/_public.html.haml b/app/views/layouts/header/_public.html.haml new file mode 100644 index 00000000000..2c5884a5b6d --- /dev/null +++ b/app/views/layouts/header/_public.html.haml @@ -0,0 +1,14 @@ +%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } + .container + .header-logo + = link_to explore_root_path, class: "home" do + = brand_header_logo + %h3 GitLab + .header-content + - unless current_controller?('sessions') + .pull-right + = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success btn-sm' + + %h1.title= title + += render 'shared/outdated_browser' diff --git a/app/views/layouts/help.html.haml b/app/views/layouts/help.html.haml new file mode 100644 index 00000000000..224b24befbe --- /dev/null +++ b/app/views/layouts/help.html.haml @@ -0,0 +1,4 @@ +- page_title "Help" +- header_title "Help", help_path + += render template: "layouts/application" diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index c57216f01c8..a3191593dae 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -1,19 +1,64 @@ -%ul +%ul.nav.nav-sidebar = nav_link(controller: :dashboard, html_options: {class: 'home'}) do = link_to admin_root_path, title: "Stats" do - Overview + = icon('dashboard fw') + %span + Overview = nav_link(controller: :projects) do - = link_to "Projects", admin_projects_path + = link_to admin_namespaces_projects_path, title: 'Projects', data: {placement: 'right'} do + = icon('cube fw') + %span + Projects = nav_link(controller: :users) do - = link_to "Users", admin_users_path + = link_to admin_users_path, title: 'Users', data: {placement: 'right'} do + = icon('user fw') + %span + Users = nav_link(controller: :groups) do - = link_to "Groups", admin_groups_path + = link_to admin_groups_path, title: 'Groups', data: {placement: 'right'} do + = icon('group fw') + %span + Groups + = nav_link(controller: :deploy_keys) do + = link_to admin_deploy_keys_path, title: 'Deploy Keys', data: {placement: 'right'} do + = icon('key fw') + %span + Deploy Keys = nav_link(controller: :logs) do - = link_to "Logs", admin_logs_path + = link_to admin_logs_path, title: 'Logs', data: {placement: 'right'} do + = icon('file-text fw') + %span + Logs = nav_link(controller: :broadcast_messages) do - = link_to "Messages", admin_broadcast_messages_path + = link_to admin_broadcast_messages_path, title: 'Broadcast Messages', data: {placement: 'right'} do + = icon('bullhorn fw') + %span + Messages = nav_link(controller: :hooks) do - = link_to "Hooks", admin_hooks_path + = link_to admin_hooks_path, title: 'Hooks', data: {placement: 'right'} do + = icon('external-link fw') + %span + Hooks = nav_link(controller: :background_jobs) do - = link_to "Background Jobs", admin_background_jobs_path + = link_to admin_background_jobs_path, title: 'Background Jobs', data: {placement: 'right'} do + = icon('cog fw') + %span + Background Jobs + = nav_link(controller: :applications) do + = link_to admin_applications_path, title: 'Applications', data: {placement: 'right'} do + = icon('cloud fw') + %span + Applications + + = nav_link(controller: :services) do + = link_to admin_application_settings_services_path, title: 'Service Templates', data: {placement: 'right'} do + = icon('copy fw') + %span + Service Templates + + = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do + = link_to admin_application_settings_path, title: 'Settings', data: {placement: 'right'} do + = icon('cogs fw') + %span + Settings diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index a6e9772d93f..687c1fc3dd2 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,18 +1,38 @@ -%ul - = nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do - = link_to root_path, title: 'Home', class: 'shortcuts-activity' do - Activity - = nav_link(path: 'dashboard#projects') do - = link_to projects_dashboard_path, class: 'shortcuts-projects' do - Projects +%ul.nav.nav-sidebar + = nav_link(path: ['dashboard#show', 'root#show'], html_options: {class: 'home'}) do + = link_to dashboard_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do + = icon('dashboard fw') + %span + Your Projects + = nav_link(path: 'projects#starred') do + = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do + = icon('star fw') + %span + Starred Projects + = nav_link(controller: :groups) do + = link_to dashboard_groups_path, title: 'Groups', data: {placement: 'right'} do + = icon('group fw') + %span + Groups + = nav_link(controller: :milestones) do + = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do + = icon('clock-o fw') + %span + Milestones = nav_link(path: 'dashboard#issues') do - = link_to issues_dashboard_path, class: 'shortcuts-issues' do - Issues - %span.count= current_user.assigned_issues.opened.count + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do + = icon('exclamation-circle fw') + %span + Issues + %span.count= current_user.assigned_issues.opened.count = nav_link(path: 'dashboard#merge_requests') do - = link_to merge_requests_dashboard_path, class: 'shortcuts-merge_requests' do - Merge Requests - %span.count= current_user.assigned_merge_requests.opened.count + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do + = icon('tasks fw') + %span + Merge Requests + %span.count= current_user.assigned_merge_requests.opened.count = nav_link(controller: :help) do - = link_to "Help", help_path - + = link_to help_path, title: 'Help', data: {placement: 'right'} do + = icon('question-circle fw') + %span + Help diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml new file mode 100644 index 00000000000..66870e84ceb --- /dev/null +++ b/app/views/layouts/nav/_explore.html.haml @@ -0,0 +1,18 @@ +%ul.nav.nav-sidebar + = nav_link(path: 'projects#trending') do + = link_to explore_root_path, title: 'Trending Projects', data: {placement: 'right'} do + = icon('comments fw') + %span Trending Projects + = nav_link(path: 'projects#starred') do + = link_to starred_explore_projects_path, title: 'Most-starred Projects', data: {placement: 'right'} do + = icon('star fw') + %span Most-starred Projects + = nav_link(path: 'projects#index') do + = link_to explore_projects_path, title: 'All Projects', data: {placement: 'right'} do + = icon('bookmark fw') + %span All Projects + = nav_link(controller: :groups) do + = link_to explore_groups_path, title: 'All Groups', data: {placement: 'right'} do + = icon('group fw') + %span All Groups + diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 9095a843c9f..9f1654b25b4 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,25 +1,53 @@ -%ul +%ul.nav.nav-sidebar = nav_link(path: 'groups#show', html_options: {class: 'home'}) do - = link_to group_path(@group), title: "Home" do - Activity - = nav_link(controller: [:group, :milestones]) do - = link_to group_milestones_path(@group) do - Milestones + = link_to group_path(@group), title: 'Home', data: {placement: 'right'} do + = icon('dashboard fw') + %span + Activity + - if current_user + = nav_link(controller: [:group, :milestones]) do + = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} do + = icon('clock-o fw') + %span + Milestones = nav_link(path: 'groups#issues') do - = link_to issues_group_path(@group) do - Issues - - if current_user - %span.count= current_user.assigned_issues.opened.of_group(@group).count + = link_to issues_group_path(@group), title: 'Issues', data: {placement: 'right'} do + = icon('exclamation-circle fw') + %span + Issues + - if current_user + %span.count= Issue.opened.of_group(@group).count = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group) do - Merge Requests - - if current_user - %span.count= current_user.cared_merge_requests.opened.of_group(@group).count - = nav_link(path: 'groups#members') do - = link_to "Members", members_group_path(@group) + = link_to merge_requests_group_path(@group), title: 'Merge Requests', data: {placement: 'right'} do + = icon('tasks fw') + %span + Merge Requests + - if current_user + %span.count= MergeRequest.opened.of_group(@group).count + = nav_link(controller: [:group_members]) do + = link_to group_group_members_path(@group), title: 'Members', data: {placement: 'right'} do + = icon('users fw') + %span + Members - - if can?(current_user, :manage_group, @group) - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), class: "tab " do - Settings + - if can?(current_user, :admin_group, @group) + = nav_link(html_options: { class: "#{"active" if group_settings_page?} separate-item" }) do + = link_to edit_group_path(@group), title: 'Settings', class: 'tab no-highlight', data: {placement: 'right'} do + = icon ('cogs fw') + %span + Settings + = icon ('angle-down fw') + + - if group_settings_page? + %ul.sidebar-subnav + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), title: 'Group', data: {placement: 'right'} do + = icon('pencil-square-o') + %span + Group Settings + = nav_link(path: 'groups#projects') do + = link_to projects_group_path(@group), title: 'Projects', data: {placement: 'right'} do + = icon('folder') + %span + Projects diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 1de5ee99cf4..914e1b83d1f 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,26 +1,51 @@ -%ul +%ul.nav.nav-sidebar = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do - = link_to profile_path, title: "Profile" do - Profile - = nav_link(controller: :accounts) do - = link_to "Account", profile_account_path + = link_to profile_path, title: 'Profile', data: {placement: 'right'} do + = icon('user fw') + %span + Profile + = nav_link(controller: [:accounts, :two_factor_auths]) do + = link_to profile_account_path, title: 'Account', data: {placement: 'right'} do + = icon('gear fw') + %span + Account + = nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new', 'applications#create']) do + = link_to applications_profile_path, title: 'Applications', data: {placement: 'right'} do + = icon('cloud fw') + %span + Applications = nav_link(controller: :emails) do - = link_to profile_emails_path do - Emails - %span.count= current_user.emails.count + 1 + = link_to profile_emails_path, title: 'Emails', data: {placement: 'right'} do + = icon('envelope-o fw') + %span + Emails + %span.count= current_user.emails.count + 1 - unless current_user.ldap_user? = nav_link(controller: :passwords) do - = link_to "Password", edit_profile_password_path + = link_to edit_profile_password_path, title: 'Password', data: {placement: 'right'} do + = icon('lock fw') + %span + Password = nav_link(controller: :notifications) do - = link_to "Notifications", profile_notifications_path + = link_to profile_notifications_path, title: 'Notifications', data: {placement: 'right'} do + = icon('inbox fw') + %span + Notifications + = nav_link(controller: :keys) do - = link_to profile_keys_path do - SSH Keys - %span.count= current_user.keys.count - = nav_link(path: 'profiles#design') do - = link_to "Design", design_profile_path - = nav_link(controller: :groups) do - = link_to "Groups", profile_groups_path + = link_to profile_keys_path, title: 'SSH Keys', data: {placement: 'right'} do + = icon('key fw') + %span + SSH Keys + %span.count= current_user.keys.count + = nav_link(controller: :preferences) do + = link_to profile_preferences_path, title: 'Preferences', data: {placement: 'right'} do + -# TODO (rspeicher): Better icon? + = icon('image fw') + %span + Preferences = nav_link(path: 'profiles#history') do - = link_to "History", history_profile_path - + = link_to history_profile_path, title: 'History', data: {placement: 'right'} do + = icon('history fw') + %span + History diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 6cb2a82bac8..cbcf560d0af 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,45 +1,92 @@ -%ul.project-navigation - = nav_link(path: 'projects#show', html_options: {class: "home"}) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do - Project +%ul.project-navigation.nav.nav-sidebar + = nav_link(path: 'projects#show', html_options: {class: 'home'}) do + = link_to project_path(@project), title: 'Project', class: 'shortcuts-project', data: {placement: 'right'} do + = icon('dashboard fw') + %span + Project - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do - = link_to 'Files', project_tree_path(@project, @ref || @repository.root_ref), class: 'shortcuts-tree' + = link_to namespace_project_tree_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Files', class: 'shortcuts-tree', data: {placement: 'right'} do + = icon('files-o fw') + %span + Files - if project_nav_tab? :commits = nav_link(controller: %w(commit commits compare repositories tags branches)) do - = link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref), class: 'shortcuts-commits' + = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do + = icon('history fw') + %span + Commits - if project_nav_tab? :network = nav_link(controller: %w(network)) do - = link_to "Network", project_network_path(@project, @ref || @repository.root_ref), class: 'shortcuts-network' + = link_to namespace_project_network_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do + = icon('code-fork fw') + %span + Network - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do - = link_to "Graphs", project_graph_path(@project, @ref || @repository.root_ref), class: 'shortcuts-graphs' + = link_to namespace_project_graph_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Graphs', class: 'shortcuts-graphs', data: {placement: 'right'} 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', data: {placement: 'right'} do + = icon('clock-o fw') + %span + Milestones - if project_nav_tab? :issues - = nav_link(controller: %w(issues milestones labels)) do - = link_to url_for_project_issues, class: 'shortcuts-issues' do - Issues - - if @project.used_default_issues_tracker? - %span.count.issue_counter= @project.issues.opened.count + = nav_link(controller: :issues) do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do + = icon('exclamation-circle fw') + %span + Issues + - if @project.default_issues_tracker? + %span.count.issue_counter= @project.issues.opened.count - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do - = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests' do - Merge Requests - %span.count.merge_counter= @project.merge_requests.opened.count + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do + = icon('tasks fw') + %span + Merge Requests + %span.count.merge_counter= @project.merge_requests.opened.count + + - if project_nav_tab? :settings + = nav_link(controller: [:project_members, :teams]) do + = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} 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', data: {placement: 'right'} do + = icon('tags fw') + %span + Labels - if project_nav_tab? :wiki = nav_link(controller: :wikis) do - = link_to 'Wiki', project_wiki_path(@project, :home), class: 'shortcuts-wiki' + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki', data: {placement: 'right'} do + = icon('book fw') + %span + Wiki - if project_nav_tab? :snippets = nav_link(controller: :snippets) do - = link_to 'Snippets', project_snippets_path(@project), class: 'shortcuts-snippets' + = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets', data: {placement: 'right'} do + = icon('file-text-o fw') + %span + Snippets - if project_nav_tab? :settings - = nav_link(html_options: {class: "#{project_tab_class}"}) do - = link_to edit_project_path(@project), class: "stat-tab tab " do - Settings + = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do + = link_to edit_project_path(@project), title: 'Settings', class: 'stat-tab tab no-highlight', data: {placement: 'right'} do + = icon('cogs fw') + %span + Settings diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml new file mode 100644 index 00000000000..633c6ae6bfb --- /dev/null +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -0,0 +1,36 @@ +%ul.project-navigation.nav.nav-sidebar + = nav_link do + = link_to project_path(@project), title: 'Back to project', data: {placement: 'right'} do + = icon('caret-square-o-left fw') + %span + Back to project + + %li.separate-item + + %ul.project-settings-nav.sidebar-subnav + = nav_link(path: 'projects#edit') do + = link_to edit_project_path(@project), title: 'Project', class: 'stat-tab tab', data: {placement: 'right'} do + = icon('pencil-square-o fw') + %span + Project Settings + = nav_link(controller: :deploy_keys) do + = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys', data: {placement: 'right'} do + = icon('key fw') + %span + Deploy Keys + = nav_link(controller: :hooks) do + = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks', data: {placement: 'right'} do + = icon('link fw') + %span + Web Hooks + = nav_link(controller: :services) do + = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services', data: {placement: 'right'} 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', data: {placement: 'right'} do + = icon('lock fw') + %span + Protected branches + diff --git a/app/views/layouts/nav/_snippets.html.haml b/app/views/layouts/nav/_snippets.html.haml new file mode 100644 index 00000000000..458b76a2c99 --- /dev/null +++ b/app/views/layouts/nav/_snippets.html.haml @@ -0,0 +1,12 @@ +%ul.nav.nav-sidebar + - if current_user + = nav_link(path: user_snippets_path(current_user), html_options: {class: 'home'}) do + = link_to user_snippets_path(current_user), title: 'Your snippets', data: {placement: 'right'} do + = icon('dashboard fw') + %span + Your Snippets + = nav_link(path: snippets_path) do + = link_to snippets_path, title: 'Discover snippets', data: {placement: 'right'} do + = icon('globe fw') + %span + Discover Snippets diff --git a/app/views/layouts/navless.html.haml b/app/views/layouts/navless.html.haml deleted file mode 100644 index 2c5fffe384f..00000000000 --- a/app/views/layouts/navless.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: @title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/broadcast" - = render "layouts/head_panel", title: @title - .container.navless-container - .content - = render "layouts/flash" - = yield diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index da451961327..ee1b57278b6 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -16,7 +16,18 @@ font-size:small; color:#777 } - + pre.commit-message { + white-space: pre-wrap; + } + .file-stats a { + text-decoration: none; + } + .file-stats .new-file { + color: #090; + } + .file-stats .deleted-file { + color: #B00; + } %body %div.content = yield @@ -24,8 +35,8 @@ %p \— %br - - if @project - You're receiving this notification because you are a member of the #{link_to_unless @target_url, @project.name_with_namespace, project_url(@project)} project team. - if @target_url #{link_to "View it on GitLab", @target_url} = email_action @target_url + - if @project && !@disable_footer + You're receiving this notification because you are a member of the #{link_to_unless @target_url, @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} project team. diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index 1d0ab84d26f..3193206fe12 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,12 +1,5 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: "Profile" - %body{class: "#{app_theme} profile", :'data-page' => body_data_page} - = render "layouts/broadcast" - = render "layouts/head_panel", title: "Profile" - %nav.main-nav.navbar-collapse.collapse - .container= render 'layouts/nav/profile' - .container - .content - = render "layouts/flash" - = yield +- page_title "Settings" +- header_title "Settings", profile_path +- sidebar "profile" + += render template: "layouts/application" diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml new file mode 100644 index 00000000000..44afa33dfe5 --- /dev/null +++ b/app/views/layouts/project.html.haml @@ -0,0 +1,14 @@ +- page_title @project.name_with_namespace +- header_title project_title(@project) +- sidebar "project" unless sidebar + +- content_for :scripts_body_top do + - if current_user + :javascript + window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; + window.markdown_preview_path = "#{markdown_preview_namespace_project_path(@project.namespace, @project)}"; + +- content_for :scripts_body do + = render "layouts/init_auto_complete" if current_user + += render template: "layouts/application" diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml index c8b8f4ba971..43401668334 100644 --- a/app/views/layouts/project_settings.html.haml +++ b/app/views/layouts/project_settings.html.haml @@ -1,19 +1,4 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: @project.name_with_namespace - %body{class: "#{app_theme} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } - = render "layouts/broadcast" - = render "layouts/head_panel", title: project_title(@project) - = render "layouts/init_auto_complete" - - if can?(current_user, :download_code, @project) - = render 'shared/no_ssh' - %nav.main-nav.navbar-collapse.collapse - .container= render 'layouts/nav/project' - .container - .content - = render "layouts/flash" - .row - .col-md-2 - = render "projects/settings_nav" - .col-md-10 - = yield +- page_title "Settings" +- sidebar "project_settings" + += render template: "layouts/project" diff --git a/app/views/layouts/projects.html.haml b/app/views/layouts/projects.html.haml deleted file mode 100644 index 8ad2f165946..00000000000 --- a/app/views/layouts/projects.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: project_head_title - %body{class: "#{app_theme} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } - = render "layouts/broadcast" - = render "layouts/head_panel", title: project_title(@project) - = render "layouts/init_auto_complete" - - if can?(current_user, :download_code, @project) - = render 'shared/no_ssh' - %nav.main-nav.navbar-collapse.collapse - .container= render 'layouts/nav/project' - .container - .content - = render "layouts/flash" - = yield - = yield :embedded_scripts diff --git a/app/views/layouts/public_group.html.haml b/app/views/layouts/public_group.html.haml deleted file mode 100644 index a289b784725..00000000000 --- a/app/views/layouts/public_group.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: group_head_title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/broadcast" - = render "layouts/public_head_panel", title: "group: #{@group.name}" - %nav.main-nav.navbar-collapse.collapse - .container= render 'layouts/nav/group' - .container - .content= yield diff --git a/app/views/layouts/public_projects.html.haml b/app/views/layouts/public_projects.html.haml deleted file mode 100644 index 2a9230244f8..00000000000 --- a/app/views/layouts/public_projects.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: @project.name_with_namespace - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/broadcast" - = render "layouts/public_head_panel", title: project_title(@project) - %nav.main-nav.navbar-collapse.collapse - .container= render 'layouts/nav/project' - .container - .content= yield diff --git a/app/views/layouts/public_users.html.haml b/app/views/layouts/public_users.html.haml deleted file mode 100644 index 4aa258fea0d..00000000000 --- a/app/views/layouts/public_users.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: @title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/broadcast" - = render "layouts/public_head_panel", title: @title - .container.navless-container - .content= yield diff --git a/app/views/layouts/search.html.haml b/app/views/layouts/search.html.haml index 084ff7ec830..fd4c7ad21a7 100644 --- a/app/views/layouts/search.html.haml +++ b/app/views/layouts/search.html.haml @@ -1,10 +1,4 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: "Search" - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/broadcast" - = render "layouts/head_panel", title: "Search" - .container.navless-container - .content - = render "layouts/flash" - = yield +- page_title "Search" +- header_title "Search", search_path + += render template: "layouts/application" diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml new file mode 100644 index 00000000000..9b0f40073ab --- /dev/null +++ b/app/views/layouts/snippets.html.haml @@ -0,0 +1,5 @@ +- page_title 'Snippets' +- header_title 'Snippets', snippets_path +- sidebar "snippets" + += render template: "layouts/application" diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml index 5272dfa0ede..3fd4b04ac84 100644 --- a/app/views/notify/_note_message.html.haml +++ b/app/views/notify/_note_message.html.haml @@ -1,2 +1,2 @@ %div - = markdown(@note.note) + = markdown(@note.note, reference_only_path: false) diff --git a/app/views/notify/_reassigned_issuable_email.text.erb b/app/views/notify/_reassigned_issuable_email.text.erb index 817d030c362..855d37429d9 100644 --- a/app/views/notify/_reassigned_issuable_email.text.erb +++ b/app/views/notify/_reassigned_issuable_email.text.erb @@ -1,6 +1,6 @@ Reassigned <%= issuable.class.model_name.human.titleize %> <%= issuable.iid %> -<%= url_for([issuable.project, issuable, {only_path: false}]) %> +<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, {only_path: false}]) %> Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%> to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %> diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml index 49f160a0d5f..ac703b31edd 100644 --- a/app/views/notify/closed_issue_email.text.haml +++ b/app/views/notify/closed_issue_email.text.haml @@ -1,3 +1,3 @@ = "Issue was closed by #{@updated_by.name}" -Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)} +Issue ##{@issue.iid}: #{namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)} diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml index d6b76e906c5..59db86b08bc 100644 --- a/app/views/notify/closed_merge_request_email.text.haml +++ b/app/views/notify/closed_merge_request_email.text.haml @@ -1,6 +1,6 @@ = "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}" -Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} +Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} = merge_path_description(@merge_request, 'to') diff --git a/app/views/notify/group_access_granted_email.html.haml b/app/views/notify/group_access_granted_email.html.haml index 823ebf77347..f1916d624b6 100644 --- a/app/views/notify/group_access_granted_email.html.haml +++ b/app/views/notify/group_access_granted_email.html.haml @@ -1,4 +1,4 @@ %p - = "You have been granted #{@membership.human_access} access to group" + = "You have been granted #{@group_member.human_access} access to group" = link_to group_url(@group) do = @group.name diff --git a/app/views/notify/group_access_granted_email.text.erb b/app/views/notify/group_access_granted_email.text.erb index 331bb98d5c9..ef9617bfc16 100644 --- a/app/views/notify/group_access_granted_email.text.erb +++ b/app/views/notify/group_access_granted_email.text.erb @@ -1,4 +1,4 @@ -You have been granted <%= @membership.human_access %> access to group <%= @group.name %> +You have been granted <%= @group_member.human_access %> access to group <%= @group.name %> <%= url_for(group_url(@group)) %> diff --git a/app/views/notify/group_invite_accepted_email.html.haml b/app/views/notify/group_invite_accepted_email.html.haml new file mode 100644 index 00000000000..55efad384a7 --- /dev/null +++ b/app/views/notify/group_invite_accepted_email.html.haml @@ -0,0 +1,6 @@ +%p + #{@group_member.invite_email}, now known as + #{link_to @group_member.user.name, user_url(@group_member.user)}, + has accepted your invitation to join group + #{link_to @group.name, group_url(@group)}. + diff --git a/app/views/notify/group_invite_accepted_email.text.erb b/app/views/notify/group_invite_accepted_email.text.erb new file mode 100644 index 00000000000..f8b70f7a5a6 --- /dev/null +++ b/app/views/notify/group_invite_accepted_email.text.erb @@ -0,0 +1,3 @@ +<%= @group_member.invite_email %>, now known as <%= @group_member.user.name %>, has accepted your invitation to join group <%= @group.name %>. + +<%= group_url(@group) %> diff --git a/app/views/notify/group_invite_declined_email.html.haml b/app/views/notify/group_invite_declined_email.html.haml new file mode 100644 index 00000000000..f9525d84fac --- /dev/null +++ b/app/views/notify/group_invite_declined_email.html.haml @@ -0,0 +1,5 @@ +%p + #{@invite_email} + has declined your invitation to join group + #{link_to @group.name, group_url(@group)}. + diff --git a/app/views/notify/group_invite_declined_email.text.erb b/app/views/notify/group_invite_declined_email.text.erb new file mode 100644 index 00000000000..6c19a288d15 --- /dev/null +++ b/app/views/notify/group_invite_declined_email.text.erb @@ -0,0 +1,3 @@ +<%= @invite_email %> has declined your invitation to join group <%= @group.name %>. + +<%= group_url(@group) %> diff --git a/app/views/notify/group_member_invited_email.html.haml b/app/views/notify/group_member_invited_email.html.haml new file mode 100644 index 00000000000..163e88bfea3 --- /dev/null +++ b/app/views/notify/group_member_invited_email.html.haml @@ -0,0 +1,14 @@ +%p + You have been invited + - if inviter = @group_member.created_by + by + = link_to inviter.name, user_url(inviter) + to join group + = link_to @group.name, group_url(@group) + as #{@group_member.human_access}. + +%p + = link_to 'Accept invitation', invite_url(@token) + or + = link_to 'decline', decline_invite_url(@token) + diff --git a/app/views/notify/group_member_invited_email.text.erb b/app/views/notify/group_member_invited_email.text.erb new file mode 100644 index 00000000000..28ce4819b14 --- /dev/null +++ b/app/views/notify/group_member_invited_email.text.erb @@ -0,0 +1,4 @@ +You have been invited <%= "by #{@group_member.created_by.name} " if @group_member.created_by %>to join group <%= @group.name %> as <%= @group_member.human_access %>. + +Accept invitation: <%= invite_url(@token) %> +Decline invitation: <%= decline_invite_url(@token) %> diff --git a/app/views/notify/issue_status_changed_email.text.erb b/app/views/notify/issue_status_changed_email.text.erb index 4200881f7e8..e6ab3fcde77 100644 --- a/app/views/notify/issue_status_changed_email.text.erb +++ b/app/views/notify/issue_status_changed_email.text.erb @@ -1,4 +1,4 @@ Issue was <%= @issue_status %> by <%= @updated_by.name %> -Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> +Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %> diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml index 8750bf86e2c..b96dd0fd8ab 100644 --- a/app/views/notify/merge_request_status_email.text.haml +++ b/app/views/notify/merge_request_status_email.text.haml @@ -1,6 +1,6 @@ = "Merge Request ##{@merge_request.iid} was #{@mr_status} by #{@updated_by.name}" -Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} +Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} = merge_path_description(@merge_request, 'to') diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml index 360da60bc3f..9db75bdb19e 100644 --- a/app/views/notify/merged_merge_request_email.text.haml +++ b/app/views/notify/merged_merge_request_email.text.haml @@ -1,6 +1,6 @@ = "Merge Request ##{@merge_request.iid} was merged" -Merge Request Url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} +Merge Request Url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} = merge_path_description(@merge_request, 'to') diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index f2f8eee18c4..53a068be52e 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,5 +1,5 @@ -if @issue.description - = markdown(@issue.description) + = markdown(@issue.description, reference_only_path: false) - if @issue.assignee_id.present? %p diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb index d36f54eb1ca..fc64c98038b 100644 --- a/app/views/notify/new_issue_email.text.erb +++ b/app/views/notify/new_issue_email.text.erb @@ -1,5 +1,5 @@ New Issue was created. -Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> -Author: <%= @issue.author_name %> -Asignee: <%= @issue.assignee_name %> +Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %> +Author: <%= @issue.author_name %> +Assignee: <%= @issue.assignee_name %> diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index f02d5111b22..5b7dd117c16 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -6,4 +6,4 @@ Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} -if @merge_request.description - = markdown(@merge_request.description) + = markdown(@merge_request.description, reference_only_path: false) diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index 16be4bb619f..bdcca6e4ab7 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -1,8 +1,8 @@ New Merge Request #<%= @merge_request.iid %> -<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %> +<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %> <%= merge_path_description(@merge_request, 'to') %> -Author: <%= @merge_request.author_name %> -Asignee: <%= @merge_request.assignee_name %> +Author: <%= @merge_request.author_name %> +Assignee: <%= @merge_request.assignee_name %> diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml index ebbe98dd472..4feacdaacff 100644 --- a/app/views/notify/new_user_email.html.haml +++ b/app/views/notify/new_user_email.html.haml @@ -11,4 +11,6 @@ - if @user.created_by_id %p - = link_to "Click here to set your password", edit_password_url(@user, :reset_password_token => @token) + = link_to "Click here to set your password", edit_password_url(@user, reset_password_token: @token) + %p + = reset_token_expire_message diff --git a/app/views/notify/new_user_email.text.erb b/app/views/notify/new_user_email.text.erb index 96b26879a77..dd9b71e3b84 100644 --- a/app/views/notify/new_user_email.text.erb +++ b/app/views/notify/new_user_email.text.erb @@ -5,4 +5,6 @@ The Administrator created an account for you. Now you are a member of the compan login.................. <%= @user.email %> <% if @user.created_by_id %> <%= link_to "Click here to set your password", edit_password_url(@user, :reset_password_token => @token) %> + + <%= reset_token_expire_message %> <% end %> diff --git a/app/views/notify/note_commit_email.text.erb b/app/views/notify/note_commit_email.text.erb index aab8e5cfb6c..aaeaf5fdf73 100644 --- a/app/views/notify/note_commit_email.text.erb +++ b/app/views/notify/note_commit_email.text.erb @@ -1,6 +1,6 @@ New comment for Commit <%= @commit.short_id %> -<%= url_for(project_commit_url(@note.project, id: @commit.id, anchor: "note_#{@note.id}")) %> +<%= url_for(namespace_project_commit_url(@note.project.namespace, @note.project, id: @commit.id, anchor: "note_#{@note.id}")) %> Author: <%= @note.author_name %> diff --git a/app/views/notify/note_issue_email.text.erb b/app/views/notify/note_issue_email.text.erb index 8a61f54a337..e33cbcd70f2 100644 --- a/app/views/notify/note_issue_email.text.erb +++ b/app/views/notify/note_issue_email.text.erb @@ -1,6 +1,6 @@ New comment for Issue <%= @issue.iid %> -<%= url_for(project_issue_url(@issue.project, @issue, anchor: "note_#{@note.id}")) %> +<%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue, anchor: "note_#{@note.id}")) %> Author: <%= @note.author_name %> diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb index 79e72ca16c6..1d1411992a6 100644 --- a/app/views/notify/note_merge_request_email.text.erb +++ b/app/views/notify/note_merge_request_email.text.erb @@ -1,6 +1,6 @@ New comment for Merge Request <%= @merge_request.iid %> -<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> +<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> <%= @note.author_name %> diff --git a/app/views/notify/project_access_granted_email.html.haml b/app/views/notify/project_access_granted_email.html.haml index 4596205f39b..dfc30a2d360 100644 --- a/app/views/notify/project_access_granted_email.html.haml +++ b/app/views/notify/project_access_granted_email.html.haml @@ -1,5 +1,5 @@ %p = "You have been granted #{@project_member.human_access} access to project" %p - = link_to project_url(@project) do + = link_to namespace_project_url(@project.namespace, @project) do = @project.name_with_namespace diff --git a/app/views/notify/project_access_granted_email.text.erb b/app/views/notify/project_access_granted_email.text.erb index de24feb802f..68eb1611ba7 100644 --- a/app/views/notify/project_access_granted_email.text.erb +++ b/app/views/notify/project_access_granted_email.text.erb @@ -1,4 +1,4 @@ You have been granted <%= @project_member.human_access %> access to project <%= @project.name_with_namespace %> -<%= url_for(project_url(@project)) %> +<%= url_for(namespace_project_url(@project.namespace, @project)) %> diff --git a/app/views/notify/project_invite_accepted_email.html.haml b/app/views/notify/project_invite_accepted_email.html.haml new file mode 100644 index 00000000000..7e58d30b10a --- /dev/null +++ b/app/views/notify/project_invite_accepted_email.html.haml @@ -0,0 +1,6 @@ +%p + #{@project_member.invite_email}, now known as + #{link_to @project_member.user.name, user_url(@project_member.user)}, + has accepted your invitation to join project + #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}. + diff --git a/app/views/notify/project_invite_accepted_email.text.erb b/app/views/notify/project_invite_accepted_email.text.erb new file mode 100644 index 00000000000..fcbe752114d --- /dev/null +++ b/app/views/notify/project_invite_accepted_email.text.erb @@ -0,0 +1,3 @@ +<%= @project_member.invite_email %>, now known as <%= @project_member.user.name %>, has accepted your invitation to join project <%= @project.name_with_namespace %>. + +<%= namespace_project_url(@project.namespace, @project) %> diff --git a/app/views/notify/project_invite_declined_email.html.haml b/app/views/notify/project_invite_declined_email.html.haml new file mode 100644 index 00000000000..c2d7e6f6e3a --- /dev/null +++ b/app/views/notify/project_invite_declined_email.html.haml @@ -0,0 +1,5 @@ +%p + #{@invite_email} + has declined your invitation to join project + #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}. + diff --git a/app/views/notify/project_invite_declined_email.text.erb b/app/views/notify/project_invite_declined_email.text.erb new file mode 100644 index 00000000000..484687fa51c --- /dev/null +++ b/app/views/notify/project_invite_declined_email.text.erb @@ -0,0 +1,3 @@ +<%= @invite_email %> has declined your invitation to join project <%= @project.name_with_namespace %>. + +<%= namespace_project_url(@project.namespace, @project) %> diff --git a/app/views/notify/project_member_invited_email.html.haml b/app/views/notify/project_member_invited_email.html.haml new file mode 100644 index 00000000000..79eb89616de --- /dev/null +++ b/app/views/notify/project_member_invited_email.html.haml @@ -0,0 +1,13 @@ +%p + You have been invited + - if inviter = @project_member.created_by + by + = link_to inviter.name, user_url(inviter) + to join project + = link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project) + as #{@project_member.human_access}. + +%p + = link_to 'Accept invitation', invite_url(@token) + or + = link_to 'decline', decline_invite_url(@token) diff --git a/app/views/notify/project_member_invited_email.text.erb b/app/views/notify/project_member_invited_email.text.erb new file mode 100644 index 00000000000..e0706272115 --- /dev/null +++ b/app/views/notify/project_member_invited_email.text.erb @@ -0,0 +1,4 @@ +You have been invited <%= "by #{@project_member.created_by.name} " if @project_member.created_by %>to join project <%= @project.name_with_namespace %> as <%= @project_member.human_access %>. + +Accept invitation: <%= invite_url(@token) %> +Decline invitation: <%= decline_invite_url(@token) %> diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml index fe248584e55..3cd759f1f57 100644 --- a/app/views/notify/project_was_moved_email.html.haml +++ b/app/views/notify/project_was_moved_email.html.haml @@ -2,14 +2,14 @@ Project was moved to another location %p The project is now located under - = link_to project_url(@project) do + = link_to namespace_project_url(@project.namespace, @project) do = @project.name_with_namespace %p To update the remote url in your local repository run (for ssh): -%p{ style: "background:#f5f5f5; padding:10px; border:1px solid #ddd" } +%p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" } git remote set-url origin #{@project.ssh_url_to_repo} %p or for http(s): -%p{ style: "background:#f5f5f5; padding:10px; border:1px solid #ddd" } +%p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" } git remote set-url origin #{@project.http_url_to_repo} %br diff --git a/app/views/notify/project_was_moved_email.text.erb b/app/views/notify/project_was_moved_email.text.erb index 664148fb3ba..b3f18b35a4d 100644 --- a/app/views/notify/project_was_moved_email.text.erb +++ b/app/views/notify/project_was_moved_email.text.erb @@ -1,7 +1,7 @@ Project was moved to another location The project is now located under -<%= project_url(@project) %> +<%= namespace_project_url(@project.namespace, @project) %> To update the remote url in your local repository run (for ssh): diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 3cf50bf0826..12f83aae04b 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,28 +1,66 @@ -%h3 #{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, project_url(@project)} +%h3 #{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} -%h4 Commits: +- if @compare + - if @reverse_compare + %p + %strong WARNING: + The push did not contain any new commits, but force pushed to delete the commits and changes below. -%ul - - @commits.each do |commit| - %li - %strong #{link_to commit.short_id, project_commit_url(@project, commit)} - %span by #{commit.author_name} - %pre #{commit.safe_message} + %h4 + = @reverse_compare ? "Deleted commits:" : "Commits:" -%h4 Changes: -- @diffs.each do |diff| - %li - %strong - - if diff.old_path == diff.new_path - = diff.new_path - - elsif diff.new_path && diff.old_path - #{diff.old_path} → #{diff.new_path} - - else - = diff.new_path || diff.old_path - %hr - %pre - = diff.diff - %br + %ul + - @commits.each do |commit| + %li + %strong #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)} + %div + %span by #{commit.author_name} + %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} + %pre.commit-message + = commit.safe_message -- if @compare.timeout - %h5 Huge diff. To prevent performance issues changes are hidden + %h4 #{pluralize @diffs.count, "changed file"}: + + %ul + - @diffs.each_with_index do |diff, i| + %li.file-stats + %a{href: "#{@target_url if @disable_diffs}#diff-#{i}" } + - if diff.deleted_file + %span.deleted-file + − + = diff.old_path + - elsif diff.renamed_file + = diff.old_path + → + = diff.new_path + - elsif diff.new_file + %span.new-file + + + = diff.new_path + - else + = diff.new_path + + - unless @disable_diffs + %h4 Changes: + - @diffs.each_with_index do |diff, i| + %li{id: "diff-#{i}"} + %a{href: @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 + + - if @compare.timeout + %h5 Huge diff. To prevent performance issues changes are hidden diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index 6f5f9eda2c5..97a176ed2a3 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -1,25 +1,49 @@ -#{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, project_url(@project)} - -\ -Commits: -- @commits.each do |commit| - #{link_to commit.short_id, project_commit_url(@project, commit)} by #{commit.author_name} - #{commit.safe_message} - \- - - - - -\ -\ -Changes: -- @diffs.each do |diff| +#{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{@project.name_with_namespace} +- if @compare \ - \===================================== - - if diff.old_path == diff.new_path - = diff.new_path - - elsif diff.new_path && diff.old_path - #{diff.old_path} → #{diff.new_path} - - else - = diff.new_path || diff.old_path - \===================================== - != diff.diff -\ -- if @compare.timeout - Huge diff. To prevent performance issues it was hidden + \ + - if @reverse_compare + WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below. + \ + \ + = @reverse_compare ? "Deleted commits:" : "Commits:" + - @commits.each do |commit| + #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} + #{commit.safe_message} + \- - - - - + \ + \ + #{pluralize @diffs.count, "changed file"}: + \ + - @diffs.each do |diff| + - if diff.deleted_file + \- − #{diff.old_path} + - elsif diff.renamed_file + \- #{diff.old_path} → #{diff.new_path} + - elsif diff.new_file + \- + #{diff.new_path} + - else + \- #{diff.new_path} + - unless @disable_diffs + \ + \ + Changes: + - @diffs.each do |diff| + \ + \===================================== + - 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 @compare.timeout + \ + \ + Huge diff. To prevent performance issues it was hidden + - if @target_url + \ + \ + View it on GitLab: #{@target_url} diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index a21dcff41c0..378dfa2dce0 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,17 +1,18 @@ +- page_title "Account" %h3.page-title - Account settings + = page_title %p.light - You can change your username and private token here. - - if current_user.ldap_user? - Some options are unavailable for LDAP accounts + Change your username and basic account settings. %hr - +- if current_user.ldap_user? + .alert.alert-info + Some options are unavailable for LDAP accounts .account-page - %fieldset.update-token - %legend - Private token - %div + .panel.panel-default.update-token + .panel-heading + Reset Private token + .panel-body = form_for @user, url: reset_private_token_profile_path, method: :put do |f| .data %p @@ -25,53 +26,91 @@ - if current_user.private_token = text_field_tag "token", current_user.private_token, class: "form-control" %div - = f.submit 'Reset', data: { confirm: "Are you sure?" }, class: "btn btn-primary btn-build-token" + = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default btn-build-token" - else %span You don`t have one yet. Click generate to fix it. - = f.submit 'Generate', class: "btn success btn-build-token" + = f.submit 'Generate', class: "btn btn-default btn-build-token" + - unless current_user.ldap_user? + .panel.panel-default + .panel-heading + Two-factor Authentication + .panel-body + - if current_user.two_factor_enabled? + .pull-right + = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm', + data: { confirm: 'Are you sure?' } + %p.text-success + %strong + Two-factor Authentication is enabled + %p + If you lose your recovery codes you can + %strong + = succeed ',' do + = link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' } + invalidating all previous codes. + + - else + %p + Increase your account's security by enabling two-factor authentication (2FA). + %p + Each time you log in you’ll be required to provide your username and + password as usual, plus a randomly-generated code from your phone. + %div + = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' - if show_profile_social_tab? - %fieldset - %legend Social Accounts - .oauth_select_holder.append-bottom-10 - %p Click on icon to activate signin with one of the following services - - enabled_social_providers.each do |provider| - %span{class: oauth_active_class(provider) } - = link_to authbutton(provider, 32), omniauth_authorize_path(User, provider) + .panel.panel-default + .panel-heading + Connected Accounts + .panel-body + .oauth-buttons.append-bottom-10 + %p Click on icon to activate signin with one of the following services + - enabled_social_providers.each do |provider| + .btn-group + = link_to oauth_image_tag(provider), omniauth_authorize_path(User, provider), + method: :post, class: "btn btn-lg #{'active' if oauth_active?(provider)}" + - if oauth_active?(provider) + = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do + = icon('close') - if show_profile_username_tab? - %fieldset.update-username - %legend - Username - = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f| - %p - Changing your username will change path to all personal projects! - %div - = f.text_field :username, required: true, class: 'form-control' - - .loading-gif.hide + .panel.panel-warning.update-username + .panel-heading + Change Username + .panel-body + = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f| %p - %i.fa.fa-spinner.fa-spin - Saving new username - %p.light - = user_url(@user) - %div - = f.submit 'Save username', class: "btn btn-save" + Changing your username will change path to all personal projects! + %div + = f.text_field :username, required: true, class: 'form-control' + + .loading-gif.hide + %p + = icon('spinner spin') + Saving new username + %p.light + = user_url(@user) + %div + = f.submit 'Save username', class: "btn btn-warning" - if show_profile_remove_tab? - %fieldset.remove-account - %legend + .panel.panel-danger.remove-account + .panel-heading Remove account - %div - %p Deleting an account has the following effects: - %ul - %li All user content like authored issues, snippets, comments will be removed - - rp = current_user.personal_projects.count - - unless rp.zero? - %li #{pluralize rp, 'personal project'} will be removed and cannot be restored - - if current_user.solo_owned_groups.present? - %li - The following groups will be abandoned. You should transfer or remove them: - %strong #{current_user.solo_owned_groups.map(&:name).join(', ')} - = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" + .panel-body + - if @user.can_be_removed? + %p Deleting an account has the following effects: + %ul + %li All user content like authored issues, snippets, comments will be removed + - rp = current_user.personal_projects.count + - unless rp.zero? + %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" + - else + - if @user.solo_owned_groups.present? + %p + Your account is currently an owner in these groups: + %strong #{@user.solo_owned_groups.map(&:name).join(', ')} + %p + You must transfer ownership or delete these groups before you can delete your account. diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml new file mode 100644 index 00000000000..2c4f0804f0b --- /dev/null +++ b/app/views/profiles/applications.html.haml @@ -0,0 +1,60 @@ +- page_title "Applications" +%h3.page-title + = page_title +%p.light + - if user_oauth_applications? + Manage applications that can use GitLab as an OAuth provider, + and applications that you've authorized to use your account. + - else + Manage applications that you've authorized to use your account. +%hr + +- if user_oauth_applications? + .oauth-applications + %h3 + Your applications + .pull-right + = link_to 'New Application', new_oauth_application_path, class: 'btn btn-success' + - if @applications.any? + %table.table.table-striped + %thead + %tr + %th Name + %th Callback URL + %th Clients + %th + %th + %tbody + - @applications.each do |application| + %tr{:id => "application_#{application.id}"} + %td= link_to application.name, oauth_application_path(application) + %td + - application.redirect_uri.split.each do |uri| + %div= uri + %td= application.access_tokens.count + %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm' + %td= render 'doorkeeper/applications/delete_form', application: application + +.oauth-authorized-applications.prepend-top-20 + - if user_oauth_applications? + %h3 + Authorized applications + + - if @authorized_tokens.any? + %table.table.table-striped + %thead + %tr + %th Name + %th Authorized At + %th Scope + %th + %tbody + - @authorized_apps.each do |app| + - token = app.authorized_tokens.order('created_at desc').first + %tr{:id => "application_#{app.id}"} + %td= app.name + %td= token.created_at + %td= token.scopes + %td= render 'doorkeeper/authorized_applications/delete_form', application: app + - else + %p.light You dont have any authorized applications diff --git a/app/views/profiles/design.html.haml b/app/views/profiles/design.html.haml deleted file mode 100644 index 0d8075b7d43..00000000000 --- a/app/views/profiles/design.html.haml +++ /dev/null @@ -1,48 +0,0 @@ -%h3.page-title - My appearance settings -%p.light - Appearance settings saved to your profile and available across all devices -%hr - -= form_for @user, url: profile_path, remote: true, method: :put do |f| - %fieldset.application-theme - %legend - Application theme - .themes_opts - = label_tag do - .prev.default - = f.radio_button :theme_id, 1 - Default - - = label_tag do - .prev.classic - = f.radio_button :theme_id, 2 - Classic - - = label_tag do - .prev.modern - = f.radio_button :theme_id, 3 - Modern - - = label_tag do - .prev.gray - = f.radio_button :theme_id, 4 - Gray - - = label_tag do - .prev.violet - = f.radio_button :theme_id, 5 - Violet - %br - .clearfix - - %fieldset.code-preview-theme - %legend - Code preview theme - .code_highlight_opts - - color_schemes.each do |color_scheme_id, color_scheme| - = label_tag do - .prev - = image_tag "#{color_scheme}-scheme-preview.png" - = f.radio_button :color_scheme_id, color_scheme_id - = color_scheme.gsub(/[-_]+/, ' ').humanize diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index ca980db2f3c..66812872c41 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -1,14 +1,27 @@ +- page_title "Emails" %h3.page-title - My email addresses + = page_title %p.light - Your - %b Primary Email - will be used for account notifications, avatar detection and web based operations, such as edits and merges. - %br - All email addresses will be used to identify your commits. - + Control emails linked to your account %hr + +%ul + %li + Your + %b Primary Email + will be used for avatar detection and web based operations, such as edits and merges. + %li + Your + %b Notification Email + will be used for account notifications. + %li + Your + %b Public Email + will be displayed on your public profile. + %li + All email addresses will be used to identify your commits. + .panel.panel-default .panel-heading Emails (#{@emails.count + 1}) @@ -16,12 +29,20 @@ %li %strong= @primary %span.label.label-success Primary Email + - if @primary === current_user.public_email + %span.label.label-info Public Email + - if @primary === current_user.notification_email + %span.label.label-info Notification Email - @emails.each do |email| %li %strong= email.email + - if email.email === current_user.public_email + %span.label.label-info Public Email + - if email.email === current_user.notification_email + %span.label.label-info Notification Email %span.cgray added #{time_ago_with_tooltip(email.created_at)} - = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-small btn-remove pull-right' + = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right' %h4 Add email address = form_for 'email', url: profile_emails_path, html: { class: 'form-horizontal' } do |f| @@ -30,4 +51,4 @@ .col-sm-10 = f.text_field :email, class: 'form-control' .form-actions - = f.submit 'Add', class: 'btn btn-create' + = f.submit 'Add email address', class: 'btn btn-create' diff --git a/app/views/profiles/groups/index.html.haml b/app/views/profiles/groups/index.html.haml deleted file mode 100644 index e9ffca8faf4..00000000000 --- a/app/views/profiles/groups/index.html.haml +++ /dev/null @@ -1,39 +0,0 @@ -%h3.page-title - Group membership - - if current_user.can_create_group? - %span.pull-right - = link_to new_group_path, class: "btn btn-new" do - %i.fa.fa-plus - New Group -%p.light - Group members have access to all a group's projects -%hr -.panel.panel-default - .panel-heading - %strong Groups - (#{@user_groups.count}) - %ul.well-list - - @user_groups.each do |user_group| - - group = user_group.group - %li - .pull-right - - if can?(current_user, :manage_group, group) - = link_to edit_group_path(group), class: "btn-small btn btn-grouped" do - %i.fa.fa-cogs - Settings - - - if can?(current_user, :destroy, user_group) - = link_to leave_profile_group_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-small btn btn-grouped", title: 'Remove user from group' do - %i.fa.fa-sign-out - Leave - - = link_to group, class: 'group-name' do - %strong= group.name - - as - %strong #{user_group.human_access} - - %div.light - #{pluralize(group.projects.count, "project")}, #{pluralize(group.users.count, "user")} - -= paginate @user_groups diff --git a/app/views/profiles/history.html.haml b/app/views/profiles/history.html.haml index 3951c47b5f2..b414fb69f4e 100644 --- a/app/views/profiles/history.html.haml +++ b/app/views/profiles/history.html.haml @@ -1,7 +1,8 @@ +- page_title "History" %h3.page-title - Account history + Your Account History %p.light - All events created by your account are listed here + All events created by your account are listed below. %hr .profile_history = render @events diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index 81411a7565e..9bbccbc45ea 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -1,9 +1,11 @@ -%li - = link_to profile_key_path(key) do - %strong= key.title - %span - (#{key.fingerprint}) - %span.cgray - added #{time_ago_with_tooltip(key.created_at)} - - = link_to 'Remove', profile_key_path(key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-small btn-remove delete-key pull-right" +%tr + %td + = link_to path_to_key(key, is_admin) do + %strong= key.title + %td + %code.key-fingerprint= key.fingerprint + %td + %span.cgray + added #{time_ago_with_tooltip(key.created_at)} + %td + = link_to 'Remove', path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right" diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml new file mode 100644 index 00000000000..e0ae4d9720f --- /dev/null +++ b/app/views/profiles/keys/_key_details.html.haml @@ -0,0 +1,22 @@ +- is_admin = defined?(admin) ? true : false +.row + .col-md-4 + .panel.panel-default + .panel-heading + SSH Key + %ul.well-list + %li + %span.light Title: + %strong= @key.title + %li + %span.light Created on: + %strong= @key.created_at.stamp("Aug 21, 2011") + + .col-md-8 + %p + %span.light Fingerprint: + %code.key-fingerprint= @key.fingerprint + %pre.well-pre + = @key.key + .pull-right + = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml new file mode 100644 index 00000000000..ef0075aad3b --- /dev/null +++ b/app/views/profiles/keys/_key_table.html.haml @@ -0,0 +1,19 @@ +- is_admin = defined?(admin) ? true : false +.panel.panel-default + - if @keys.any? + %table.table + %thead.panel-heading + %tr + %th Title + %th Fingerprint + %th Added at + %th + %tbody + - @keys.each do |key| + = render 'profiles/keys/key', key: key, is_admin: is_admin + - else + .nothing-here-block + - if is_admin + User has no ssh keys + - else + There are no SSH keys with access to your account. diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index a322f82f236..06655f7ba3a 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,22 +1,11 @@ +- page_title "SSH Keys" %h3.page-title - My SSH keys + = page_title .pull-right = link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new" %p.light - SSH keys allow you to establish a secure connection between your computer and GitLab - %br Before you can add an SSH key you need to - = link_to "generate it", help_page_path("ssh", "ssh") + = link_to "generate it.", help_page_path("ssh", "README") %hr - -.panel.panel-default - .panel-heading - SSH Keys (#{@keys.count}) - %ul.well-list#keys-table - = render @keys - - if @keys.blank? - %li - .nothing-here-block There are no SSH keys with access to your account. - - += render 'key_table' diff --git a/app/views/profiles/keys/new.html.haml b/app/views/profiles/keys/new.html.haml index c02b47b0ad5..2bf207a3221 100644 --- a/app/views/profiles/keys/new.html.haml +++ b/app/views/profiles/keys/new.html.haml @@ -1,3 +1,4 @@ +- page_title "Add SSH Keys" %h3.page-title Add an SSH Key %p.light Paste your public key here. Read more about how to generate a key on #{link_to "the SSH help page", help_page_path("ssh", "README")}. @@ -8,9 +9,9 @@ $('#key_key').on('focusout', function(){ var title = $('#key_title'), val = $('#key_key').val(), - key_mail = val.match(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+|\.[a-zA-Z0-9._-]+)/gi); + comment = val.match(/^\S+ \S+ (.+)$/); - if( key_mail && key_mail.length > 0 && title.val() == '' ){ - $('#key_title').val( key_mail ); + if( comment && comment.length > 1 && title.val() == '' ){ + $('#key_title').val( comment[1] ); } }); diff --git a/app/views/profiles/keys/show.html.haml b/app/views/profiles/keys/show.html.haml index c4fc1bb269c..89f6f01581a 100644 --- a/app/views/profiles/keys/show.html.haml +++ b/app/views/profiles/keys/show.html.haml @@ -1,22 +1,2 @@ -.row - .col-md-4 - .panel.panel-default - .panel-heading - SSH Key - %ul.well-list - %li - %span.light Title: - %strong= @key.title - %li - %span.light Created on: - %strong= @key.created_at.stamp("Aug 21, 2011") - - .col-md-8 - %p - %span.light Fingerprint: - %strong= @key.fingerprint - %pre.well-pre - = @key.key - -.pull-right - = link_to 'Remove', profile_key_path(@key), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" +- page_title @key.title, "SSH Keys" += render "key_details" diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index a044fad8fa3..9480a19f5b2 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,33 +1,57 @@ +- page_title "Notifications" %h3.page-title - Notifications settings + = page_title %p.light - GitLab uses the email specified in your profile for notifications + These are your global notification settings. %hr -= form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications form-horizontal global-notifications-form' do + += form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications form-horizontal global-notifications-form' } do |f| + -if @user.errors.any? + %div.alert.alert-danger + %ul + - @user.errors.full_messages.each do |msg| + %li= msg + = hidden_field_tag :notification_type, 'global' - = label_tag :notification_level, 'Notification level', class: 'control-label' - .col-sm-10 - .radio - = label_tag nil, class: '' do - = radio_button_tag :notification_level, Notification::N_DISABLED, @notification.disabled?, class: 'trigger-submit' - .level-title - Disabled - %p You will not get any notifications via email - - .radio - = label_tag nil, class: '' do - = radio_button_tag :notification_level, Notification::N_PARTICIPATING, @notification.participating?, class: 'trigger-submit' - .level-title - Participating - %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) - - .radio - = label_tag nil, class: '' do - = radio_button_tag :notification_level, Notification::N_WATCH, @notification.watch?, class: 'trigger-submit' - .level-title - Watch - %p You will receive all notifications from projects in which you participate + .form-group + = f.label :notification_email, class: "control-label" + .col-sm-10 + = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "form-control" + + .form-group + = f.label :notification_level, class: 'control-label' + .col-sm-10 + .radio + = f.label :notification_level, value: Notification::N_DISABLED do + = f.radio_button :notification_level, Notification::N_DISABLED + .level-title + Disabled + %p You will not get any notifications via email + + .radio + = f.label :notification_level, value: Notification::N_MENTION do + = f.radio_button :notification_level, Notification::N_MENTION + .level-title + Mention + %p You will receive notifications only for comments in which you were @mentioned + + .radio + = f.label :notification_level, value: Notification::N_PARTICIPATING do + = f.radio_button :notification_level, Notification::N_PARTICIPATING + .level-title + Participating + %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) + + .radio + = f.label :notification_level, value: Notification::N_WATCH do + = f.radio_button :notification_level, Notification::N_WATCH + .level-title + Watch + %p You will receive all notifications from projects in which you participate + + .form-actions + = f.submit 'Save changes', class: "btn btn-create" .clearfix %hr @@ -36,16 +60,16 @@ %p You can also specify notification level per group or per project. %br - By default all projects and groups uses notification level set above. + By default, all projects and groups will use the notification level set above. %h4 Groups: %ul.bordered-list - - @group_members.each do |users_group| - - notification = Notification.new(users_group) - = render 'settings', type: 'group', membership: users_group, notification: notification + - @group_members.each do |group_member| + - notification = Notification.new(group_member) + = render 'settings', type: 'group', membership: group_member, notification: notification .col-md-6 %p - To specify notification level per project of a group you belong to, + To specify the notification level per project of a group you belong to, %br you need to be a member of the project itself, not only its group. %h4 Projects: diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 425200ff523..399ae98adf9 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,33 +1,40 @@ -%h3.page-title Password +- page_title "Password" +%h3.page-title + = page_title %p.light - Change your password or recover your current one. + - if @user.password_automatically_set? + Set your password. + - else + Change your password or recover your current one. %hr .update-password = form_for @user, url: profile_password_path, method: :put, html: { class: 'form-horizontal' } do |f| %div %p.slead - You must provide current password in order to change it. - %br - After a successful password update you will be redirected to login page where you should login with your new password + - unless @user.password_automatically_set? + You must provide current password in order to change it. + %br + After a successful password update, you will be redirected to the login page where you can log in with your new password. -if @user.errors.any? .alert.alert-danger %ul - @user.errors.full_messages.each do |msg| %li= msg - .form-group - = f.label :current_password, class: 'control-label' - .col-sm-10 - = f.password_field :current_password, required: true, class: 'form-control' - %div - = link_to "Forgot your password?", reset_profile_password_path, method: :put + - unless @user.password_automatically_set? + .form-group + = f.label :current_password, class: 'control-label' + .col-sm-10 + = f.password_field :current_password, required: true, class: 'form-control' + %div + = link_to "Forgot your password?", reset_profile_password_path, method: :put .form-group = f.label :password, 'New password', class: 'control-label' .col-sm-10 - = f.password_field :password, required: true, class: 'form-control', id: 'user_password_profile' + = f.password_field :password, required: true, class: 'form-control' .form-group = f.label :password_confirmation, class: 'control-label' .col-sm-10 = f.password_field :password_confirmation, required: true, class: 'form-control' .form-actions - = f.submit 'Save password', class: "btn btn-save" + = f.submit 'Save password', class: "btn btn-create" diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml index 42d2d0db29c..9c6204963e0 100644 --- a/app/views/profiles/passwords/new.html.haml +++ b/app/views/profiles/passwords/new.html.haml @@ -1,3 +1,5 @@ +- page_title "New Password" +- header_title "New Password" %h3.page-title Setup new password %hr = form_for @user, url: profile_password_path, method: :post, html: { class: 'form-horizontal '} do |f| @@ -10,13 +12,14 @@ %ul - @user.errors.full_messages.each do |msg| %li= msg - - .form-group - = f.label :current_password, class: 'control-label' - .col-sm-10= f.password_field :current_password, required: true, class: 'form-control' + + - unless @user.password_automatically_set? + .form-group + = f.label :current_password, class: 'control-label' + .col-sm-10= f.password_field :current_password, required: true, class: 'form-control' .form-group = f.label :password, class: 'control-label' - .col-sm-10= f.password_field :password, required: true, class: 'form-control', id: 'user_password_profile' + .col-sm-10= f.password_field :password, required: true, class: 'form-control' .form-group = f.label :password_confirmation, class: 'control-label' .col-sm-10 diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml new file mode 100644 index 00000000000..aa99280fde6 --- /dev/null +++ b/app/views/profiles/preferences/show.html.haml @@ -0,0 +1,42 @@ +- page_title 'Preferences' +%h3.page-title + = page_title +%p.light + These settings allow you to customize the appearance and behavior of the site. + They are saved with your account and will persist to any device you use to + access the site. +%hr + += form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'js-preferences-form form-horizontal'} do |f| + .panel.panel-default.application-theme + .panel-heading + Application theme + .panel-body + - Gitlab::Themes.each do |theme| + = label_tag do + .preview{class: theme.css_class} + = f.radio_button :theme_id, theme.id + = theme.name + + .panel.panel-default.syntax-theme + .panel-heading + Syntax highlighting theme + .panel-body + - color_schemes.each do |color_scheme_id, color_scheme| + = label_tag do + .preview= image_tag "#{color_scheme}-scheme-preview.png" + = f.radio_button :color_scheme_id, color_scheme_id + = color_scheme.tr('-_', ' ').titleize + + .panel.panel-default + .panel-heading + Behavior + .panel-body + .form-group + = f.label :dashboard, class: 'control-label' do + Default Dashboard + = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank') + .col-sm-10 + = f.select :dashboard, dashboard_choices, {}, class: 'form-control' + .panel-footer + = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb new file mode 100644 index 00000000000..6c4b0ce757d --- /dev/null +++ b/app/views/profiles/preferences/update.js.erb @@ -0,0 +1,9 @@ +// Remove body class for any previous theme, re-add current one +$('body').removeClass('<%= Gitlab::Themes.body_classes %>') +$('body').addClass('<%= user_application_theme %>') + +// Re-enable the "Save" button +$('input[type=submit]').enable() + +// Show the notice flash message +new Flash('<%= flash.discard(:notice) %>', 'notice') diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 640104fdad1..37a3952635e 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,7 +1,8 @@ +- page_title "Profile" %h3.page-title - Profile settings + = page_title %p.light - This information appears on your profile. + This information will appear on your profile. - if current_user.ldap_user? Some options are unavailable for LDAP accounts %hr @@ -36,12 +37,20 @@ = f.text_field :email, class: "form-control", required: true - if @user.unconfirmed_email.present? %span.help-block - Please click the link in the confirmation email before continuing, it was sent to - %strong #{@user.unconfirmed_email} + Please click the link in the confirmation email before continuing. It was sent to + = succeed "." do + %strong #{@user.unconfirmed_email} + %p + = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post - else %span.help-block We also use email for avatar detection if no avatar is uploaded. .form-group + = f.label :public_email, class: "control-label" + .col-sm-10 + = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show in profile'}, class: "form-control" + %span.help-block This email will be displayed on your public profile. + .form-group = f.label :skype, class: "control-label" .col-sm-10= f.text_field :skype, class: "form-control" .form-group @@ -54,9 +63,12 @@ = f.label :website_url, 'Website', class: "control-label" .col-sm-10= f.text_field :website_url, class: "form-control" .form-group + = f.label :location, 'Location', class: "control-label" + .col-sm-10= f.text_field :location, class: "form-control" + .form-group = f.label :bio, class: "control-label" .col-sm-10 - = f.text_area :bio, rows: 6, class: "form-control", maxlength: 250 + = f.text_area :bio, rows: 4, class: "form-control", maxlength: 250 %span.help-block Tell us about yourself in fewer than 250 characters. .col-md-5 @@ -77,7 +89,7 @@ %br or change it at #{link_to "gravatar.com", "http://gravatar.com"} %hr - %a.choose-btn.btn.btn-small.js-choose-user-avatar-button + %a.choose-btn.btn.btn-sm.js-choose-user-avatar-button %i.fa.fa-paperclip %span Choose File ... @@ -86,13 +98,16 @@ .light The maximum file size allowed is 200KB. - if @user.avatar? %hr - = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" + = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" - if @user.public_profile? - .bs-callout.bs-callout-info + .alert.alert-info %h4 Public profile %p Your profile is publicly visible because you joined public project(s) - .form-actions - = f.submit 'Save changes', class: "btn btn-save" + .row + .col-md-7 + .form-group + .col-sm-offset-2.col-sm-10 + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml new file mode 100644 index 00000000000..1b1395eaa17 --- /dev/null +++ b/app/views/profiles/two_factor_auths/_codes.html.haml @@ -0,0 +1,11 @@ +%p.slead + Should you ever lose your phone, each of these recovery codes can be used one + time each to regain access to your account. Please save them in a safe place. + +.codes.well + %ul + - @codes.each do |code| + %li + %span.monospace= code + += link_to 'Proceed', profile_account_path, class: 'btn btn-success' diff --git a/app/views/profiles/two_factor_auths/codes.html.haml b/app/views/profiles/two_factor_auths/codes.html.haml new file mode 100644 index 00000000000..addf356697a --- /dev/null +++ b/app/views/profiles/two_factor_auths/codes.html.haml @@ -0,0 +1,5 @@ +- page_title 'Recovery Codes', 'Two-factor Authentication' + +%h3.page-title Two-factor Authentication Recovery codes +%hr += render 'codes' diff --git a/app/views/profiles/two_factor_auths/create.html.haml b/app/views/profiles/two_factor_auths/create.html.haml new file mode 100644 index 00000000000..e330aadac13 --- /dev/null +++ b/app/views/profiles/two_factor_auths/create.html.haml @@ -0,0 +1,6 @@ +- page_title 'Two-factor Authentication', 'Account' + +.alert.alert-success + Congratulations! You have enabled Two-factor Authentication! + += render 'codes' diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml new file mode 100644 index 00000000000..74268c9bde2 --- /dev/null +++ b/app/views/profiles/two_factor_auths/new.html.haml @@ -0,0 +1,40 @@ +- page_title 'Two-factor Authentication', 'Account' + +%h2.page-title Two-Factor Authentication (2FA) +%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('workflow', 'two_factor_authentication'))}. + +%hr + += form_tag profile_two_factor_auth_path, method: :post, class: 'form-horizontal two-factor-new' do |f| + - if @error + .alert.alert-danger + = @error + .form-group + .col-lg-2.col-lg-offset-2 + = raw @qr_code + .col-lg-7.col-lg-offset-1.manual-instructions + %h3 Can't scan the code? + + %p + To add the entry manually, provide the following details to the + application on your phone. + + %dl + %dt Account + %dd= current_user.email + %dl + %dt Key + %dd= current_user.otp_secret.scan(/.{4}/).join(' ') + %dl + %dt Time based + %dd Yes + .form-group + = label_tag :pin_code, nil, class: "control-label" + .col-lg-10 + = text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true + .form-actions + = submit_tag 'Submit', class: 'btn btn-success' diff --git a/app/views/profiles/update.js.erb b/app/views/profiles/update.js.erb deleted file mode 100644 index 04b5cf4827d..00000000000 --- a/app/views/profiles/update.js.erb +++ /dev/null @@ -1,9 +0,0 @@ -// Remove body class for any previous theme, re-add current one -$('body').removeClass('ui_basic ui_mars ui_modern ui_gray ui_color') -$('body').addClass('<%= app_theme %>') - -// Re-render the header to reflect the new theme -$('header').html('<%= escape_javascript(render("layouts/head_panel", title: "Profile")) %>') - -// Re-initialize header tooltips -$('.has_bottom_tooltip').tooltip({placement: 'bottom'}) diff --git a/app/views/projects/_aside.html.haml b/app/views/projects/_aside.html.haml new file mode 100644 index 00000000000..c9c17110d2b --- /dev/null +++ b/app/views/projects/_aside.html.haml @@ -0,0 +1,108 @@ +.clearfix + - unless @project.empty_repo? + .panel.panel-default + .panel-heading + = visibility_level_icon(@project.visibility_level) + = "#{visibility_level_label(@project.visibility_level).capitalize} project" + + .panel-body + - if @repository.changelog || @repository.license || @repository.contribution_guide + %ul.nav.nav-pills + - if @repository.changelog + %li.hidden-xs + = link_to changelog_url(@project) do + Changelog + - if @repository.license + %li + = link_to license_url(@project) do + License + - if @repository.contribution_guide + %li + = link_to contribution_guide_url(@project) do + Contribution guide + + .actions + - if can? current_user, :write_issue, @project + = link_to url_for_new_issue(@project, only_path: true), title: "New Issue", class: 'btn btn-sm append-right-10' do + = icon("exclamation-circle fw") + New Issue + + - if can? current_user, :write_merge_request, @project + = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-sm", title: "New Merge Request" do + = icon("plus fw") + New Merge Request + + - if forked_from_project = @project.forked_from_project + .panel-footer + = icon("code-fork fw") + Forked from + .pull-right + = link_to forked_from_project.namespace.try(:name), project_path(forked_from_project) + + + - @project.ci_services.each do |ci_service| + - if ci_service.active? && ci_service.respond_to?(:builds_path) + .panel-footer + = icon("check fw") + = ci_service.title + .pull-right + - if ci_service.respond_to?(:status_img_path) + = link_to ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' do + = image_tag ci_service.status_img_path, alt: "build status", class: 'ci-status-image' + - else + = link_to 'view builds', ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' + + + - unless @project.empty_repo? + .panel.panel-default + .panel-heading + = icon("folder-o fw") + Repository + .panel-body + %ul.nav.nav-pills + %li + = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) do + = pluralize(number_with_delimiter(@repository.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') + + .actions + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-sm append-right-10' do + %i.fa.fa-exchange + Compare code + + - if can?(current_user, :download_code, @project) + = render 'projects/repositories/download_archive', split_button: true, btn_class: 'btn-group-sm' + - if version = @repository.version + .panel-footer + = icon("clock-o fw") + Version + .pull-right + = link_to version_url(@project) do + = @repository.blob_by_oid(version.id).data + + = render "shared/clone_panel" + + - if @project.archived? + %br + .alert.alert-warning + %h4 + = icon("exclamation-triangle fw") + Archived project! + %p Repository is read-only + + - if current_user + - access = user_max_access_in_project(current_user, @project) + - if access + .light-well.light.prepend-top-20 + %small + You have #{access} access to this project. + - if @project.project_member_by_id(current_user) + %br + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), + data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do + Leave this project diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml new file mode 100644 index 00000000000..745163e79a7 --- /dev/null +++ b/app/views/projects/_bitbucket_import_modal.html.haml @@ -0,0 +1,13 @@ +%div#bitbucket_import_modal.modal + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3 Import projects from Bitbucket + .modal-body + To enable importing projects from Bitbucket, + - if current_user.admin? + you need to + - else + your GitLab administrator needs to + == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/bitbucket.md'}. diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml index fd8320adb8d..35f7e7bb34b 100644 --- a/app/views/projects/_commit_button.html.haml +++ b/app/views/projects/_commit_button.html.haml @@ -2,8 +2,5 @@ .commit-button-annotation = button_tag 'Commit Changes', class: 'btn commit-btn js-commit-button btn-create' - .message - to branch - %strong= ref = link_to 'Cancel', cancel_path, class: 'btn btn-cancel', data: {confirm: leave_edit_message} diff --git a/app/views/projects/_dropdown.html.haml b/app/views/projects/_dropdown.html.haml deleted file mode 100644 index 6ff46970336..00000000000 --- a/app/views/projects/_dropdown.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -- if current_user - .dropdown.pull-right - %a.dropdown-toggle.btn.btn-new{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-bars - %ul.dropdown-menu - - if @project.issues_enabled && can?(current_user, :write_issue, @project) - %li - = link_to url_for_new_issue, title: "New Issue" do - New issue - - if @project.merge_requests_enabled && can?(current_user, :write_merge_request, @project) - %li - = link_to new_project_merge_request_path(@project), title: "New Merge Request" do - New merge request - - if @project.snippets_enabled && can?(current_user, :write_snippet, @project) - %li - = link_to new_project_snippet_path(@project), title: "New Snippet" do - New snippet - - if can?(current_user, :admin_team_member, @project) - %li - = link_to new_project_team_member_path(@project), title: "New project member" do - New project member - - if can? current_user, :push_code, @project - %li.divider - %li - = link_to new_project_branch_path(@project) do - %i.fa.fa-code-fork - Git branch - %li - = link_to new_project_tag_path(@project) do - %i.fa.fa-tag - Git tag - - diff --git a/app/views/projects/_github_import_modal.html.haml b/app/views/projects/_github_import_modal.html.haml new file mode 100644 index 00000000000..de58b27df23 --- /dev/null +++ b/app/views/projects/_github_import_modal.html.haml @@ -0,0 +1,13 @@ +%div#github_import_modal.modal + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3 Import projects from GitHub + .modal-body + To enable importing projects from GitHub, + - if current_user.admin? + you need to + - else + your GitLab administrator needs to + == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/github.md'}.
\ No newline at end of file diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml new file mode 100644 index 00000000000..ae6c25f9371 --- /dev/null +++ b/app/views/projects/_gitlab_import_modal.html.haml @@ -0,0 +1,13 @@ +%div#gitlab_import_modal.modal + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3 Import projects from GitLab.com + .modal-body + To enable importing projects from GitLab.com, + - if current_user.admin? + you need to + - else + your GitLab administrator needs to + == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/gitlab.md'}.
\ No newline at end of file diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 30d063c7a36..076afb11a9d 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,42 +1,37 @@ - empty_repo = @project.empty_repo? -.project-home-panel{:class => ("empty-project" if empty_repo)} - .project-home-row +.project-home-panel.clearfix{:class => ("empty-project" if empty_repo)} + .project-identicon-holder + = project_icon(@project, alt: '', class: 'avatar project-avatar') + .project-home-row.project-home-row-top .project-home-desc - if @project.description.present? - = escaped_autolink(@project.description) + = markdown(@project.description, pipeline: :description) - if can?(current_user, :admin_project, @project) – - = link_to 'Edit', edit_project_path - - elsif !@project.empty_repo? && @repository.readme + = link_to 'Edit', edit_namespace_project_path + - elsif !empty_repo && @repository.readme - readme = @repository.readme – - = link_to project_blob_path(@project, tree_join(@repository.root_ref, readme.name)) do + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do = readme.name - .star-fork-buttons - - unless @project.empty_repo? - .fork-buttons - - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace + .project-repo-buttons + .inline.star.js-toggler-container{class: @show_star ? 'on' : ''} + - if current_user + = link_to_toggle_star('Star this project.', false) + = link_to_toggle_star('Unstar this project.', true) + - else + = link_to new_user_session_path, class: 'btn star-btn has_tooltip', title: 'You must sign in to star a project' do + %span + = icon('star') + Star + %span.count + = @project.star_count + - unless empty_repo + - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace + .inline.fork-buttons.prepend-left-10 - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to project_path(current_user.fork_of(@project)), title: 'Go to my fork' do + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-sm btn-default' do = link_to_toggle_fork - else - = link_to new_project_fork_path(@project), title: "Fork project" do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-sm btn-default' do = link_to_toggle_fork - - .star-buttons - %span.star.js-toggler-container{class: @show_star ? 'on' : ''} - - if current_user - = link_to_toggle_star('Star this project.', false, true) - = link_to_toggle_star('Unstar this project.', true, true) - - else - = link_to_toggle_star('You must sign in to star a project.', false, false) - - .project-home-row.hidden-xs - - if current_user && !empty_repo - .project-home-dropdown - = render "dropdown" - - unless @project.empty_repo? - - if can? current_user, :download_code, @project - .pull-right.prepend-left-10 - = render 'projects/repositories/download_archive', split_button: true - = render "shared/clone_panel" diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index dd40a719561..496fad34dc2 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -1,6 +1,6 @@ - if issuable.errors.any? .row - .col-sm-10.col-sm-offset-2 + .col-sm-offset-2.col-sm-10 .alert.alert-danger - issuable.errors.full_messages.each do |msg| %span= msg @@ -11,20 +11,32 @@ .col-sm-10 = f.text_field :title, maxlength: 255, autofocus: true, class: 'form-control pad js-gfm-input', required: true + + - if issuable.is_a?(MergeRequest) + %p.help-block + - if issuable.work_in_progress? + Remove the <code>WIP</code> prefix from the title to allow this + <strong>Work In Progress</strong> merge request to be accepted when it's ready. + - else + Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a + <strong>Work In Progress</strong> merge request from being accepted before it's ready. .form-group.issuable-description = f.label :description, 'Description', class: 'control-label' .col-sm-10 - = render 'projects/zen', f: f, attr: :description, - classes: 'description form-control' - .col-sm-12.hint - .pull-left - Parsed with - #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}. - .pull-right - Attach images (JPG, PNG, GIF) by dragging & dropping - or #{link_to 'selecting them', '#', class: 'markdown-selector' }. - .clearfix - .error-alert + + = render layout: 'projects/md_preview', locals: { preview_class: "wiki", referenced_users: true } do + = render 'projects/zen', f: f, attr: :description, + classes: 'description form-control' + .col-sm-12.hint + .pull-left + Parsed with + #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}. + .pull-right + Attach files by dragging & dropping + or #{link_to 'selecting them', '#', class: 'markdown-selector' }. + + .clearfix + .error-alert %hr .form-group .issue-assignee @@ -32,9 +44,9 @@ %i.fa.fa-user Assign to .col-sm-10 - = project_users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select a user', class: 'custom-form-control', - selected: issuable.assignee_id) + = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", + placeholder: 'Select a user', class: 'custom-form-control', null_user: true, + selected: issuable.assignee_id, project: @target_project || @project) = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' .form-group @@ -47,23 +59,51 @@ = f.select(:milestone_id, milestone_options(issuable), { include_blank: 'Select milestone' }, { class: 'select2' }) - else + .prepend-top-10 %span.light No open milestones available. - = link_to 'Create new milestone', new_project_milestone_path(issuable.project), target: :blank + - 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 = f.label :label_ids, class: 'control-label' do - %i.icon-tag + %i.fa.fa-tag Labels .col-sm-10 - if issuable.project.labels.any? = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, { selected: issuable.label_ids }, multiple: true, class: 'select2' - else + .prepend-top-10 %span.light No labels yet. - = link_to 'Create new label', new_project_label_path(issuable.project), target: :blank + - 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 + +- if issuable.is_a?(MergeRequest) + %hr + - if @merge_request.new_record? + .form-group + = f.label :source_branch, class: 'control-label' do + %i.fa.fa-code-fork + Source Branch + .col-sm-10 + = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) + .form-group + = f.label :target_branch, class: 'control-label' do + %i.fa.fa-code-fork + Target Branch + .col-sm-10 + = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record? }) + - if @merge_request.new_record? + %p.help-block + = link_to 'Change branches', mr_change_branches_path(@merge_request) .form-actions + - if !issuable.project.empty_repo? && (guide_url = contribution_guide_url(issuable.project)) && !issuable.persisted? + %p + Please review the + %strong #{link_to 'guidelines for contribution', guide_url} + to this repository. - if issuable.new_record? = f.submit "Submit new #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - else @@ -72,4 +112,4 @@ - cancel_project = issuable.source_project - else - cancel_project = issuable.project - = link_to 'Cancel', [cancel_project, issuable], class: 'btn btn-cancel' + = link_to 'Cancel', [cancel_project.namespace.becomes(Namespace), cancel_project, issuable], class: 'btn btn-cancel' diff --git a/app/views/projects/_issues_nav.html.haml b/app/views/projects/_issues_nav.html.haml deleted file mode 100644 index 18628eb6207..00000000000 --- a/app/views/projects/_issues_nav.html.haml +++ /dev/null @@ -1,55 +0,0 @@ -%ul.nav.nav-tabs - - if project_nav_tab? :issues - = nav_link(controller: :issues) do - = link_to project_issues_path(@project), class: "tab" do - Issues - - if project_nav_tab? :merge_requests - = nav_link(controller: :merge_requests) do - = link_to project_merge_requests_path(@project), class: "tab" do - Merge Requests - = nav_link(controller: :milestones) do - = link_to 'Milestones', project_milestones_path(@project), class: "tab" - = nav_link(controller: :labels) do - = link_to 'Labels', project_labels_path(@project), class: "tab" - - - if current_controller?(:milestones) - %li.pull-right - %button.btn.btn-default.sidebar-expand-button - %i.icon.fa.fa-list - - - if current_controller?(:issues) - - if current_user - %li.hidden-xs - = link_to project_issues_path(@project, :atom, { private_token: current_user.private_token }) do - %i.fa.fa-rss - - %li.pull-right - .pull-right - %button.btn.btn-default.sidebar-expand-button - %i.icon.fa.fa-list - - .pull-left - = form_tag project_issues_path(@project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do - .append-right-10.hidden-xs.hidden-sm - = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } - = hidden_field_tag :state, params['state'] - = hidden_field_tag :scope, params['scope'] - = hidden_field_tag :assignee_id, params['assignee_id'] - = hidden_field_tag :milestone_id, params['milestone_id'] - = hidden_field_tag :label_id, params['label_id'] - - - if can? current_user, :write_issue, @project - = link_to new_project_issue_path(@project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do - %i.fa.fa-plus - New Issue - - - if current_controller?(:merge_requests) - %li.pull-right - .pull-right - %button.btn.btn-default.sidebar-expand-button - %i.icon.fa.fa-list - - - if can? current_user, :write_merge_request, @project - = link_to new_project_merge_request_path(@project), class: "btn btn-new pull-left", title: "New Merge Request" do - %i.fa.fa-plus - New Merge Request diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml new file mode 100644 index 00000000000..b7bca6dae09 --- /dev/null +++ b/app/views/projects/_md_preview.html.haml @@ -0,0 +1,24 @@ +.md-area + .md-header.clearfix + %ul.nav.nav-tabs + %li.active + = link_to '#md-write-holder', class: 'js-md-write-button', tabindex: '-1' do + Write + %li + = link_to '#md-preview-holder', class: 'js-md-preview-button', tabindex: '-1' do + Preview + + - if defined?(referenced_users) && referenced_users + %span.referenced-users.pull-left.hide + = icon('exclamation-triangle') + You are about to add + %strong + %span.js-referenced-users-count 0 + people + to the discussion. Proceed with caution. + + %div + .md-write-holder + = yield + .md.md-preview-holder.hide + .js-md-preview{class: (preview_class if defined?(preview_class))} diff --git a/app/views/projects/_section.html.haml b/app/views/projects/_section.html.haml new file mode 100644 index 00000000000..d7b06197f67 --- /dev/null +++ b/app/views/projects/_section.html.haml @@ -0,0 +1,36 @@ +%ul.nav.nav-tabs + %li.active + = link_to '#tab-activity', 'data-toggle' => 'tab' do + = icon("tachometer") + Activity + - if @repository.readme + %li + = link_to '#tab-readme', 'data-toggle' => 'tab' do + = icon("file-text-o") + Readme +.tab-content + .tab-pane.active#tab-activity + .hidden-xs + = render "events/event_last_push", event: @last_push + + - if current_user + %ul.nav.nav-pills.event_filter.pull-right + %li + = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'rss-btn' do + %i.fa.fa-rss + + = render 'shared/event_filter' + %hr + .content_list + = spinner + + - if readme = @repository.readme + .tab-pane#tab-readme + %article.readme-holder#README + .clearfix + %small.pull-right + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light' do + %i.fa.fa-file + = readme.name + .wiki + = render_readme(readme) diff --git a/app/views/projects/_settings_nav.html.haml b/app/views/projects/_settings_nav.html.haml deleted file mode 100644 index 2008f8c558d..00000000000 --- a/app/views/projects/_settings_nav.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -%ul.nav.nav-pills.nav-stacked.nav-stacked-menu.append-bottom-20.project-settings-nav - = nav_link(path: 'projects#edit') do - = link_to edit_project_path(@project), class: "stat-tab tab " do - %i.fa.fa-pencil-square-o - Project - = nav_link(controller: [:team_members, :teams]) do - = link_to project_team_index_path(@project), class: "team-tab tab" do - %i.fa.fa-users - Members - = nav_link(controller: :deploy_keys) do - = link_to project_deploy_keys_path(@project) do - %i.fa.fa-key - Deploy Keys - = nav_link(controller: :hooks) do - = link_to project_hooks_path(@project) do - %i.fa.fa-link - Web Hooks - = nav_link(controller: :services) do - = link_to project_services_path(@project) do - %i.fa.fa-cogs - Services - = nav_link(controller: :protected_branches) do - = link_to project_protected_branches_path(@project) do - %i.fa.fa-lock - Protected branches diff --git a/app/views/projects/_visibility_level.html.haml b/app/views/projects/_visibility_level.html.haml deleted file mode 100644 index 5f34e66b3ed..00000000000 --- a/app/views/projects/_visibility_level.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -.form-group.project-visibility-level-holder - = f.label :visibility_level, class: 'control-label' do - Visibility Level - = link_to "(?)", help_page_path("public_access", "public_access") - .col-sm-10 - - if can_change_visibility_level - - Gitlab::VisibilityLevel.values.each do |level| - .radio - - restricted = restricted_visibility_levels.include?(level) - = f.radio_button :visibility_level, level, checked: (visibility_level == level), disabled: restricted - = label :project_visibility_level, level do - = visibility_level_icon(level) - .option-title - = visibility_level_label(level) - .option-descr - = visibility_level_description(level) - - unless restricted_visibility_levels.empty? - .col-sm-10 - %span.info - Some visibility level settings have been restricted by the administrator. - - else - .col-sm-10 - %span.info - = visibility_level_icon(visibility_level) - %strong - = visibility_level_label(visibility_level) - .light= visibility_level_description(visibility_level) diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index 2bbc49e8eb5..cf1c55ecca6 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,7 +1,10 @@ .zennable - %input#zen-toggle-comment{ tabindex: '-1', type: 'checkbox' } + %input#zen-toggle-comment.zen-toggle-comment{ tabindex: '-1', type: 'checkbox' } .zen-backdrop - classes << ' js-gfm-input markdown-area' = f.text_area attr, class: classes, placeholder: 'Leave a comment' - %label{ for: 'zen-toggle-comment', class: 'expand' } Edit in fullscreen - %label{ for: 'zen-toggle-comment', class: 'collapse' } + = link_to nil, class: 'zen-enter-link', tabindex: '-1' do + %i.fa.fa-expand + Edit in fullscreen + = link_to nil, class: 'zen-leave-link' do + %i.fa.fa-compress diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index bdf02c6285d..8019c7f4569 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,34 +1,36 @@ +- page_title "Blame", @blob.path, @ref %h3.page-title Blame view #tree-holder.tree-holder .file-holder .file-title %i.fa.fa-file - %span.file_name + %strong = @path - %small= number_to_human_size @blob.size - %span.options= render "projects/blob/actions" + %small= number_to_human_size @blob.size + .file-actions + = render "projects/blob/actions" .file-content.blame.highlight %table - @blame.each do |commit, lines, since| - - commit = Commit.new(commit) + - commit = Commit.new(commit, @project) %tr %td.blame-commit %span.commit - = link_to commit.short_id, project_commit_path(@project, commit), class: "commit_short_id" + = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "commit_short_id" = commit_author_link(commit, avatar: true, size: 16) - = link_to_gfm truncate(commit.title, length: 20), project_commit_path(@project, commit.id), class: "row_title" + = link_to_gfm truncate(commit.title, length: 20), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "row_title" %td.lines.blame-numbers %pre - (since...(since + lines.count)).each do |i| = i \ %td.lines - %pre - %code{ class: highlightjs_class(@blob.name) } + %pre{class: 'code highlight white'} + %code :erb <% lines.each do |line| %> - <%= line %> + <%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %> <% end %> diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 812d88a8730..13f8271b979 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -1,28 +1,22 @@ .btn-group.tree-btn-group - -# only show edit link for text files - - if @blob.text? - - if allowed_tree_edit? - = link_to 'Edit', project_edit_tree_path(@project, @id), - class: 'btn btn-small' - - else - %span.btn.btn-small.disabled Edit - = link_to 'Raw', project_raw_path(@project, @id), - class: 'btn btn-small', target: '_blank' + = edit_blob_link(@project, @ref, @path) + = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), + class: 'btn btn-sm', target: '_blank' -# only show normal/blame view links for text files - if @blob.text? - - if current_page? project_blame_path(@project, @id) - = link_to 'Normal View', project_blob_path(@project, @id), - class: 'btn btn-small' + - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) + = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), + class: 'btn btn-sm' - else - = link_to 'Blame', project_blame_path(@project, @id), - class: 'btn btn-small' unless @blob.empty? - = link_to 'History', project_commits_path(@project, @id), - class: 'btn btn-small' + = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id), + class: 'btn btn-sm' unless @blob.empty? + = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), + class: 'btn btn-sm' - if @ref != @commit.sha - = link_to 'Permalink', project_blob_path(@project, - tree_join(@commit.sha, @path)), class: 'btn btn-small' + = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, + tree_join(@commit.sha, @path)), class: 'btn btn-sm' - if allowed_tree_edit? - = button_tag class: 'remove-blob btn btn-small btn-remove', + = button_tag class: 'remove-blob btn btn-sm btn-remove', 'data-toggle' => 'modal', 'data-target' => '#modal-remove-blob' do Remove diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 492dff437b5..65c3ab10e02 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -1,32 +1,34 @@ %ul.breadcrumb.repo-breadcrumb %li %i.fa.fa-angle-right - = link_to project_tree_path(@project, @ref) do + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do = @project.path - tree_breadcrumbs(@tree, 6) do |title, path| %li - if path - if path.end_with?(@path) - = link_to project_blob_path(@project, path) do + = link_to namespace_project_blob_path(@project.namespace, @project, path) do %strong = truncate(title, length: 40) - else - = link_to truncate(title, length: 40), project_tree_path(@project, path) + = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) - else = link_to title, '#' -%ul.blob-commit-info.bs-callout.bs-callout-info.hidden-xs - - blob_commit = @repository.last_commit_for_path(@commit.id, @blob.path) +%ul.blob-commit-info.well.hidden-xs + - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project %div#tree-content-holder.tree-content-holder %article.file-holder - .file-title.clearfix - %i.fa.fa-file - %span.file_name + .file-title + = blob_icon blob.mode, blob.name + %strong = blob.name - %small= number_to_human_size blob.size - %span.options.hidden-xs= render "actions" + %small + = number_to_human_size(blob.size) + .file-actions.hidden-xs + = render "actions" - if blob.text? = render "text", blob: blob - elsif blob.image? diff --git a/app/views/projects/blob/_download.html.haml b/app/views/projects/blob/_download.html.haml index c24eeea4931..f2c5e95ecf4 100644 --- a/app/views/projects/blob/_download.html.haml +++ b/app/views/projects/blob/_download.html.haml @@ -1,6 +1,6 @@ .file-content.blob_file.blob-no-preview .center - = link_to project_raw_path(@project, @id) do + = link_to namespace_project_raw_path(@project.namespace, @project, @id) do %h1.light %i.fa.fa-download %h4 diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml new file mode 100644 index 00000000000..9c3e1703c89 --- /dev/null +++ b/app/views/projects/blob/_editor.html.haml @@ -0,0 +1,25 @@ +.file-holder.file + .file-title + .editor-ref + %i.fa.fa-code-fork + = ref + %span.editor-file-name + - if @path + %span.monospace + = @path + + - if current_action?(:new) || current_action?(:create) + \/ + = text_field_tag 'file_name', params[:file_name], placeholder: "File name", + required: true, class: 'form-control new-file-name' + .pull-right + = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' + + .file-content.code + %pre.js-edit-mode-pane#editor + = params[:content] || local_assigns[:blob_data] + - if local_assigns[:path] + .js-edit-mode-pane#preview.hide + .center + %h2 + %i.icon-spinner.icon-spin diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml index c5568315cb1..b8d8451880a 100644 --- a/app/views/projects/blob/_remove.html.haml +++ b/app/views/projects/blob/_remove.html.haml @@ -1,4 +1,4 @@ -#modal-remove-blob.modal.hide +#modal-remove-blob.modal .modal-dialog .modal-content .modal-header @@ -9,12 +9,11 @@ %strong= @ref .modal-body - = form_tag project_blob_path(@project, @id), method: :delete, class: 'form-horizontal' do + = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal' do = render 'shared/commit_message_container', params: params, placeholder: 'Removed this file because...' .form-group - .col-sm-2 - .col-sm-10 + .col-sm-offset-2.col-sm-10 = button_tag 'Remove file', class: 'btn btn-remove btn-remove-file' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml index 7cbea7c3eb6..4429c395aee 100644 --- a/app/views/projects/blob/_text.html.haml +++ b/app/views/projects/blob/_text.html.haml @@ -1,13 +1,9 @@ -- if gitlab_markdown?(blob.name) - .file-content.wiki - = preserve do - = markdown(blob.data) -- elsif markup?(blob.name) +- if markup?(blob.name) .file-content.wiki = render_markup(blob.name, blob.data) - else .file-content.code - unless blob.empty? - = render 'shared/file_hljs', blob: blob + = render 'shared/file_highlight', blob: blob - else .nothing-here-block Empty file diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index 5c79d0ef11f..84742608986 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -2,7 +2,7 @@ - if @form.unfold? && @form.since != 1 && !@form.bottom? %tr.line_holder{ id: @form.since } = render "projects/diffs/match_line", {line: @match_line, - line_old: @form.since, line_new: @form.since, bottom: false} + line_old: @form.since, line_new: @form.since, bottom: false, new_file: false} - @lines.each_with_index do |line, index| - line_new = index + @form.since @@ -16,4 +16,4 @@ - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to } = render "projects/diffs/match_line", {line: @match_line, - line_old: @form.to, line_new: @form.to, bottom: true} + line_old: @form.to, line_new: @form.to, bottom: true, new_file: false} diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml new file mode 100644 index 00000000000..e78181f8801 --- /dev/null +++ b/app/views/projects/blob/edit.html.haml @@ -0,0 +1,32 @@ +- page_title "Edit", @blob.path, @ref +.file-editor + %ul.nav.nav-tabs.js-edit-mode + %li.active + = link_to '#editor' do + %i.fa.fa-edit + Edit file + + %li + = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do + %i.fa.fa-eye + = editing_preview_title(@blob.name) + + = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: "form-horizontal") do + = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data + = render 'shared/commit_message_container', params: params, + placeholder: "Update #{@blob.name}" + + .form-group.branch + = label_tag 'branch', class: 'control-label' do + Branch + .col-sm-10 + = text_field_tag 'new_branch', @ref, class: "form-control" + + = hidden_field_tag 'last_commit', @last_commit + = hidden_field_tag 'content', '', id: "file-content" + = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] + = render 'projects/commit_button', ref: @ref, + cancel_path: @after_edit_path + +:javascript + blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}") diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml new file mode 100644 index 00000000000..f7ddf74b4fc --- /dev/null +++ b/app/views/projects/blob/new.html.haml @@ -0,0 +1,21 @@ +- page_title "New File", @ref +%h3.page-title New file +.file-editor + = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file') do + = render 'projects/blob/editor', ref: @ref + = render 'shared/commit_message_container', params: params, + placeholder: 'Add new file' + + - unless @project.empty_repo? + .form-group.branch + = label_tag 'branch', class: 'control-label' do + Branch + .col-sm-10 + = text_field_tag 'new_branch', @ref, class: "form-control" + + = hidden_field_tag 'content', '', id: 'file-content' + = render 'projects/commit_button', ref: @ref, + cancel_path: namespace_project_tree_path(@project.namespace, @project, @id) + +:javascript + blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null) diff --git a/app/views/projects/edit_tree/preview.html.haml b/app/views/projects/blob/preview.html.haml index e7c3460ad78..e7c3460ad78 100644 --- a/app/views/projects/edit_tree/preview.html.haml +++ b/app/views/projects/blob/preview.html.haml diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 69167654c39..a1d464bac59 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -1,3 +1,4 @@ +- page_title @blob.path, @ref %div.tree-ref-holder = render 'shared/ref_switcher', destination: 'blob', path: @path diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 8e58f3c247a..43412624da6 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,7 +1,7 @@ - commit = @repository.commit(branch.target) %li(class="js-branch-#{branch.name}") %h4 - = link_to project_tree_path(@project, branch.name) do + = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do %strong.str-truncated= branch.name - if branch.name == @repository.root_ref %span.label.label-info default @@ -10,16 +10,19 @@ %i.fa.fa-lock protected .pull-right - - if can?(current_user, :download_code, @project) - = render 'projects/repositories/download_archive', ref: branch.name, btn_class: 'btn-grouped btn-group-small' + - 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 project_compare_index_path(@project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-small', method: :post, title: "Compare" do - %i.fa.fa-files-o + = 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) - = link_to project_branch_path(@project, branch.name), class: 'btn btn-grouped btn-small btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do - %i.fa.fa-trash-o + = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do + = icon("trash-o") - if commit %ul.list-unstyled diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 9f2b1b59292..80acc937908 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -1,14 +1,15 @@ +- page_title "Branches" = render "projects/commits/head" %h3.page-title Branches .pull-right - if can? current_user, :push_code, @project - = link_to new_project_branch_path(@project), class: 'btn btn-create' do + = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do %i.fa.fa-add-sign New branch .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} %span.light sort: - if @sort.present? = @sort.humanize @@ -17,12 +18,12 @@ %b.caret %ul.dropdown-menu %li - = link_to project_branches_path(sort: nil) do + = link_to namespace_project_branches_path(sort: nil) do Name - = link_to project_branches_path(sort: 'recently_updated') do - Recently updated - = link_to project_branches_path(sort: 'last_updated') do - Last updated + = 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 %hr - unless @branches.empty? %ul.bordered-list.top-list.all-branches diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index a6623240da1..cac5dc91afd 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Branch" - if @error .alert.alert-danger %button{ type: "button", class: "close", "data-dismiss" => "alert"} × @@ -5,7 +6,7 @@ %h3.page-title %i.fa.fa-code-fork New branch -= form_tag project_branches_path, method: :post, class: "form-horizontal" do += form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal" do .form-group = label_tag :branch_name, 'Name for new branch', class: 'control-label' .col-sm-10 @@ -16,9 +17,10 @@ = text_field_tag :ref, params[:ref], placeholder: 'existing branch name, tag or commit SHA', required: true, tabindex: 2, class: 'form-control' .form-actions = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 - = link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel' + = link_to 'Cancel', namespace_project_branches_path(@project.namespace, @project), class: 'btn btn-cancel' :javascript + disableButtonIfAnyEmptyField($("#new-branch-form"), ".form-control", ".btn-create"); var availableTags = #{@project.repository.ref_names.to_json}; $("#ref").autocomplete({ diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index e149f017f84..3f645b81397 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -10,15 +10,16 @@ Download as %span.caret %ul.dropdown-menu - %li= link_to "Email Patches", project_commit_path(@project, @commit, format: :patch) - %li= link_to "Plain Diff", project_commit_path(@project, @commit, format: :diff) - = link_to project_tree_path(@project, @commit), class: "btn btn-primary btn-grouped" do + - 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-primary btn-grouped" do %span Browse Code » %div %p %span.light Commit - = link_to @commit.id, project_commit_path(@project, @commit) + = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit) .commit-info-row %span.light Authored by %strong @@ -35,20 +36,10 @@ .commit-info-row %span.cgray= pluralize(@commit.parents.count, "parent") - @commit.parents.each do |parent| - = link_to parent.short_id, project_commit_path(@project, parent) + = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent) -- if @branches.any? - .commit-info-row - %span.cgray - Exists in - %span - - branch = commit_default_branch(@project, @branches) - = link_to(branch, project_tree_path(@project, branch)) - - if @branches.any? - and in - = link_to("#{pluralize(@branches.count, "other branch")}", "#", class: "js-details-expand") - %span.js-details-content.hide - = commit_branches_links(@project, @branches) +.commit-info-row.branches + %i.fa.fa-spinner.fa-spin .commit-box %h3.commit-title @@ -56,3 +47,6 @@ - if @commit.description.present? %pre.commit-description = preserve(gfm(escape_once(@commit.description))) + +:coffeescript + $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}") diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml new file mode 100644 index 00000000000..82aac1fbd15 --- /dev/null +++ b/app/views/projects/commit/branches.html.haml @@ -0,0 +1,16 @@ +- if @branches.any? + %span + - branch = commit_default_branch(@project, @branches) + = link_to(namespace_project_tree_path(@project.namespace, @project, branch)) do + %span.label.label-gray + %i.fa.fa-code-fork + = branch + - if @branches.any? || @tags.any? + = link_to("#", class: "js-details-expand") do + %span.label.label-gray + \... + %span.js-details-content.hide + - if @branches.any? + = commit_branches_links(@project, @branches) + - if @tags.any? + = commit_tags_links(@project, @tags) diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index fc721067ed4..60b112e67d4 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,3 +1,4 @@ +- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" = render "commit_box" = render "projects/diffs/diffs", diffs: @diffs, project: @project -= render "projects/notes/notes_with_form" += render "projects/notes/notes_with_form", view: params[:view] diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 1eb17f760dc..f9106564a27 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -1,24 +1,24 @@ %li.commit.js-toggle-container .commit-row-title - = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id" - - %span.str-truncated - = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" + %strong.str-truncated + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" - if commit.description? %a.text-expander.js-toggle-button ... - = link_to_browse_code(project, commit) + .pull-right + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" .notes_count - if @note_counts - note_count = @note_counts.fetch(commit.id, 0) - else - - notes = project.notes.for_commit_id(commit.id) - - note_count = notes.count + - notes = commit.notes + - note_count = notes.user.count - if note_count > 0 - %span.label.label-gray - %i.fa.fa-comment= note_count + %span.light + %i.fa.fa-comments + = note_count - if commit.description? .commit-row-description.js-toggle-content @@ -26,6 +26,8 @@ = preserve(gfm(escape_once(commit.description))) .commit-row-info - = commit_author_link(commit, avatar: true, size: 16) + = commit_author_link(commit, avatar: true, size: 24) + authored .committed_ago - #{time_ago_with_tooltip(commit.committed_date)} + #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} + = link_to_browse_code(project, commit) diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index 2ee7d73bd20..ce60fbdf032 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -3,9 +3,9 @@ Commits (#{@commits.count}) - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE %ul.well-list - - Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE)).each do |commit| + - Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), @project).each do |commit| = render "projects/commits/inline_commit", commit: commit, project: @project %li.warning-row.unstyled other #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} commits hidden to prevent performance issues. - else - %ul.well-list= render Commit.decorate(@commits), project: @project + %ul.well-list= render Commit.decorate(@commits, @project), project: @project diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index d57659065a8..0cd9ce1f371 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -1,11 +1,15 @@ +- unless defined?(project) + - project = @project + - @commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| .row.commits-row - .col-md-2 - %h4 + .col-md-2.hidden-xs.hidden-sm + %h5.commits-row-date %i.fa.fa-calendar %span= day.stamp("28 Aug, 2010") - %p= pluralize(commits.count, 'commit') - .col-md-10 + .light + = pluralize(commits.count, 'commit') + .col-md-10.col-sm-12 %ul.bordered-list - = render commits, project: @project + = render commits, project: project %hr.lists-separator diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 0c9d906481b..e3d8cd0fdd5 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,15 +1,22 @@ %ul.nav.nav-tabs = nav_link(controller: [:commit, :commits]) do - = link_to 'Commits', project_commits_path(@project, @repository.root_ref) + = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) do + = icon("history") + Commits + %span.badge= number_with_delimiter(@repository.commit_count) = nav_link(controller: :compare) do - = link_to 'Compare', project_compare_index_path(@project, from: @repository.root_ref, to: @ref || @repository.root_ref) + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref) do + = icon("exchange") + Compare = nav_link(html_options: {class: branches_tab_class}) do - = link_to project_branches_path(@project) do + = link_to namespace_project_branches_path(@project.namespace, @project) do + = icon("code-fork") Branches %span.badge.js-totalbranch-count= @repository.branches.size = nav_link(controller: :tags) do - = link_to project_tags_path(@project) do + = link_to namespace_project_tags_path(@project.namespace, @project) do + = icon("tags") Tags %span.badge.js-totaltags-count= @repository.tags.length diff --git a/app/views/projects/commits/_inline_commit.html.haml b/app/views/projects/commits/_inline_commit.html.haml index 574599aa2d2..c03bc3f9df9 100644 --- a/app/views/projects/commits/_inline_commit.html.haml +++ b/app/views/projects/commits/_inline_commit.html.haml @@ -1,8 +1,8 @@ %li.commit.inline-commit .commit-row-title - = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id" + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" %span.str-truncated - = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" .pull-right #{time_ago_with_tooltip(commit.committed_date)} diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 32c82edb248..3854ad5d611 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -1,18 +1,18 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "Recent commits to #{@project.name}:#{@ref}" - xml.link :href => project_commits_url(@project, @ref, format: :atom), :rel => "self", :type => "application/atom+xml" - xml.link :href => project_commits_url(@project, @ref), :rel => "alternate", :type => "text/html" - xml.id project_commits_url(@project, @ref) + xml.title "#{@project.name}:#{@ref} commits" + xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html" + xml.id namespace_project_commits_url(@project.namespace, @project, @ref) xml.updated @commits.first.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") if @commits.any? @commits.each do |commit| xml.entry do - xml.id project_commit_url(@project, :id => commit.id) - xml.link :href => project_commit_url(@project, :id => commit.id) - xml.title truncate(commit.title, :length => 80) + 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.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, :width => "40", :height => "40", :url => avatar_icon(commit.author_email) + xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(commit.author_email) xml.author do |author| xml.name commit.author_name xml.email commit.author_email diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 5717c24c274..9682100a54c 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -1,21 +1,30 @@ +- page_title "Commits", @ref += 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" .tree-ref-holder = render 'shared/ref_switcher', destination: 'commits' -- if current_user && current_user.private_token - .commits-feed-holder.hidden-xs.hidden-sm - = link_to project_commits_path(@project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Feed", class: 'btn' do - %i.fa.fa-rss - Commits feed +.commits-feed-holder.hidden-xs.hidden-sm + - if create_mr_button?(@repository.root_ref, @ref) + = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do + = icon('plus') + Create Merge Request + + - if current_user && current_user.private_token + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Feed", class: 'prepend-left-10 btn' do + = icon("rss") + Commits Feed + %ul.breadcrumb.repo-breadcrumb = commits_breadcrumbs - %li.active - commits %div{id: dom_id(@project)} - #commits-list= render "commits" + #commits-list= render "commits", project: @project .clear = spinner diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index cb0a3747f7d..a0e904cfd8b 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -1,4 +1,4 @@ -= form_tag project_compare_index_path(@project), method: :post, class: 'form-inline' do += form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline' do .clearfix.append-bottom-20 - if params[:to] && params[:from] = link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has_tooltip', title: 'Switch base of comparison'} @@ -13,9 +13,10 @@ = text_field_tag :to, params[:to], class: "form-control" = button_tag "Compare", class: "btn btn-create commits-compare-btn" - - if compare_to_mr_button? - = link_to compare_mr_path, class: 'prepend-left-10 btn' do - %strong Make a merge request + - if create_mr_button? + = link_to create_mr_path, class: 'prepend-left-10 btn' do + = icon("plus") + Create Merge Request :javascript diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 4745bfbeaaf..d1e579a2ede 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Compare" = render "projects/commits/head" %h3.page-title diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 214b5bd337b..3670dd5c13b 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -1,3 +1,4 @@ +- page_title "#{params[:from]}...#{params[:to]}" = render "projects/commits/head" %h3.page-title diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index a0345dbd9c3..8d66bae8cdf 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -1,24 +1,32 @@ %li .pull-right - if @available_keys.include?(deploy_key) - = link_to enable_project_deploy_key_path(@project, deploy_key), class: 'btn btn-small', method: :put do - %i.fa.fa-plus + = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do + = icon('plus') Enable - else - - if deploy_key.projects.count > 1 - = link_to disable_project_deploy_key_path(@project, deploy_key), class: 'btn btn-small', method: :put do - %i.fa.fa-power-off - Disable + - if deploy_key.destroyed_when_orphaned? && deploy_key.almost_orphaned? + = link_to 'Remove', disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :put, class: "btn btn-remove delete-key btn-sm pull-right" - else - = link_to 'Remove', project_deploy_key_path(@project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :delete, class: "btn btn-remove delete-key btn-small pull-right" - + = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do + = icon('power-off') + Disable - = link_to project_deploy_key_path(deploy_key.projects.include?(@project) ? @project : deploy_key.projects.first, deploy_key) do - %i.fa.fa-key - %strong= deploy_key.title + = icon('key') + %strong= deploy_key.title + %br + %code.key-fingerprint= deploy_key.fingerprint %p.light.prepend-top-10 - - deploy_key.projects.map(&:name_with_namespace).each do |project_name| - %span.label.label-gray.deploy-project-label= project_name + - if deploy_key.public? + %span.label.label-info.deploy-project-label + Public deploy key + + - deploy_key.projects.each do |project| + - if can?(current_user, :read_project, project) + %span.label.label-gray.deploy-project-label + = link_to namespace_project_path(project.namespace, project) do + = project.name_with_namespace + %small.pull-right Created #{time_ago_with_tooltip(deploy_key.created_at)} diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index 162ef05b367..91675b3738e 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -1,5 +1,5 @@ %div - = form_for [@project, @key], url: project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal' } do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal' } do |f| -if @key.errors.any? .alert.alert-danger %ul @@ -19,5 +19,5 @@ .form-actions = f.submit 'Create', class: "btn-create btn" - = link_to "Cancel", project_deploy_keys_path(@project), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_deploy_keys_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml index 6f475e0b395..2e9c5dc08c8 100644 --- a/app/views/projects/deploy_keys/index.html.haml +++ b/app/views/projects/deploy_keys/index.html.haml @@ -1,7 +1,8 @@ +- page_title "Deploy Keys" %h3.page-title Deploy keys allow read-only access to the repository - = link_to new_project_deploy_key_path(@project), class: "btn btn-new pull-right", title: "New Deploy Key" do + = link_to new_namespace_project_deploy_key_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Deploy Key" do %i.fa.fa-plus New Deploy Key @@ -20,13 +21,22 @@ = render @enabled_keys - if @enabled_keys.blank? .light-well - .nothing-here-block Create a #{link_to 'new deploy key', new_project_deploy_key_path(@project)} or add an existing one + .nothing-here-block Create a #{link_to 'new deploy key', new_namespace_project_deploy_key_path(@project.namespace, @project)} or add an existing one .col-md-6.available-keys - %h5 - %strong Deploy keys - from projects available to you - %ul.bordered-list - = render @available_keys - - if @available_keys.blank? - .light-well - .nothing-here-block Deploy keys from projects you have access to will be displayed here + - # If there are available public deploy keys but no available project deploy keys, only public deploy keys are shown. + - if @available_project_keys.any? || @available_public_keys.blank? + %h5 + %strong Deploy keys + from projects you have access to + %ul.bordered-list + = render @available_project_keys + - if @available_project_keys.blank? + .light-well + .nothing-here-block Deploy keys from projects you have access to will be displayed here + + - if @available_public_keys.any? + %h5 + %strong Public deploy keys + available to any project + %ul.bordered-list + = render @available_public_keys diff --git a/app/views/projects/deploy_keys/new.html.haml b/app/views/projects/deploy_keys/new.html.haml index 186d6b58972..01c810aee18 100644 --- a/app/views/projects/deploy_keys/new.html.haml +++ b/app/views/projects/deploy_keys/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Deploy Key" %h3.page-title New Deploy key %hr diff --git a/app/views/projects/deploy_keys/show.html.haml b/app/views/projects/deploy_keys/show.html.haml deleted file mode 100644 index c66e6bc69c3..00000000000 --- a/app/views/projects/deploy_keys/show.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -%h3.page-title - Deploy key: - = @key.title - %small - created on - = @key.created_at.stamp("Aug 21, 2011") -.back-link - = link_to project_deploy_keys_path(@project) do - ← To keys list -%hr -%pre= @key.key -.pull-right - = link_to 'Remove', project_deploy_key_path(@project, @key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn-remove btn delete-key" diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 334ea1ba82f..ec8974c5475 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,22 +1,17 @@ -.row - .col-md-8 - = render 'projects/diffs/stats', diffs: diffs - .col-md-4 - %ul.nav.nav-tabs - %li.pull-right{class: params[:view] == 'parallel' ? 'active' : ''} - - params_copy = params.dup - - params_copy[:view] = 'parallel' - = link_to "Side-by-side Diff", url_for(params_copy), {id: "commit-diff-viewtype"} - %li.pull-right{class: params[:view] != 'parallel' ? 'active' : ''} - - params_copy[:view] = 'inline' - = link_to "Inline Diff", url_for(params_copy), {id: "commit-diff-viewtype"} +.prepend-top-20.append-bottom-20 + .pull-right + .btn-group + = inline_diff_btn + = parallel_diff_btn + = render 'projects/diffs/stats', diffs: diffs +- diff_files = safe_diff_files(diffs) -- if show_diff_size_warning?(diffs) - = render 'projects/diffs/warning', diffs: diffs +- if diff_files.count < diffs.size + = render 'projects/diffs/warning', diffs: diffs, shown_files_count: diff_files.count .files - - safe_diff_files(diffs).each_with_index do |diff_file, index| + - diff_files.each_with_index do |diff_file, index| = render 'projects/diffs/file', diff_file: diff_file, i: index, project: project - if @diff_timeout @@ -25,3 +20,6 @@ Failed to collect changes %p Maybe diff is really big and operation failed with timeout. Try to get diff locally + +:coffeescript + $('.files .diff-header').stick_in_parent(offset_top: $('.navbar').height()) diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index bf7770ceedf..99ee23a1ddc 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,42 +1,41 @@ - blob = project.repository.blob_for_diff(@commit, diff_file.diff) - return unless blob -- blob_diff_path = diff_project_blob_path(project, tree_join(@commit.id, diff_file.file_path)) +- blob_diff_path = namespace_project_blob_diff_path(project.namespace, project, tree_join(@commit.id, diff_file.file_path)) .diff-file{id: "diff-#{i}", data: {blob_diff_path: blob_diff_path }} .diff-header{id: "file-path-#{hexdigest(diff_file.new_path || diff_file.old_path)}"} - if diff_file.deleted_file - %span= diff_file.old_path + %span="#{diff_file.old_path} deleted" .diff-btn-group - if @commit.parent_ids.present? = view_file_btn(@commit.parent_id, diff_file, project) + - elsif diff_file.diff.submodule? + %span + - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) + = submodule_link(submodule_item, @commit.id, project.repository) - else - - if diff_file.renamed_file - %span= "#{diff_file.old_path} renamed to #{diff_file.new_path}" - - else - %span= diff_file.new_path - - if diff_file.mode_changed? - %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" + %span + - if diff_file.renamed_file + = "#{diff_file.old_path} renamed to #{diff_file.new_path}" + - else + = diff_file.new_path + - if diff_file.mode_changed? + %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" .diff-btn-group - if blob.text? - - unless params[:view] == 'parallel' - %label - = check_box_tag nil, 1, false, class: 'js-toggle-diff-line-wrap' - Wrap text - - = link_to '#', class: 'js-toggle-diff-comments btn btn-small' do - %i.fa.fa-chevron-down - Diff comments + = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do + %i.fa.fa-comments - if @merge_request && @merge_request.source_project - = link_to project_edit_tree_path(@merge_request.source_project, tree_join(@merge_request.source_branch, diff_file.new_path), from_merge_request_id: @merge_request.id), { class: 'btn btn-small' } do - Edit - + = edit_blob_link(@merge_request.source_project, + @merge_request.source_branch, diff_file.new_path, + after: ' ', from_merge_request_id: @merge_request.id) = view_file_btn(@commit.id, diff_file, project) - .diff-content + .diff-content.diff-wrap-lines -# Skipp all non non-supported blobs - return unless blob.respond_to?('text?') - if blob.text? diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 900646dd0a4..058b71b21f5 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -10,7 +10,7 @@ %div.two-up.view %span.wrap .frame.deleted - %a{href: project_blob_path(@project, tree_join(@commit.parent_id, diff.old_path))} + %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))} %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size old_file.size}" @@ -22,7 +22,7 @@ %span.meta-height %span.wrap .frame.added - %a{href: project_blob_path(@project, tree_join(@commit.id, diff.new_path))} + %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))} %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size file.size}" diff --git a/app/views/projects/diffs/_match_line.html.haml b/app/views/projects/diffs/_match_line.html.haml index 4ebe3379733..d1f897b99f7 100644 --- a/app/views/projects/diffs/_match_line.html.haml +++ b/app/views/projects/diffs/_match_line.html.haml @@ -1,7 +1,7 @@ -%td.old_line.diff-line-num.unfold.js-unfold{data: {linenumber: line_old}, - class: unfold_bottom_class(bottom)} +%td.old_line.diff-line-num{data: {linenumber: line_old}, + class: [unfold_bottom_class(bottom), unfold_class(!new_file)]} \... -%td.new_line.diff-line-num.unfold.js-unfold{data: {linenumber: line_new}, - class: unfold_bottom_class(bottom)} +%td.new_line.diff-line-num{data: {linenumber: line_new}, + class: [unfold_bottom_class(bottom), unfold_class(!new_file)]} \... %td.line_content.matched= line diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 75f3a80f0d7..cb41dd852d3 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -18,6 +18,8 @@ - elsif type_left == 'old' || type_left.nil? %td.old_line{id: line_code_left, class: "#{type_left}"} = link_to raw(line_number_left), "##{line_code_left}", id: line_code_left + - if @comments_allowed && can?(current_user, :write_note, @project) + = link_to_new_diff_note(line_code_left, 'old') %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= raw line_content_left - if type_right == 'new' @@ -29,12 +31,14 @@ %td.new_line{id: new_line_code, class: "#{new_line_class}", data: { linenumber: line_number_right }} = link_to raw(line_number_right), "##{new_line_code}", id: new_line_code + - if @comments_allowed && can?(current_user, :write_note, @project) + = link_to_new_diff_note(line_code_right, 'new') %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= raw line_content_right - if @reply_allowed - comments_left, comments_right = organize_comments(type_left, type_right, line_code_left, line_code_right) - if comments_left.present? || comments_right.present? - = render "projects/notes/diff_notes_with_reply_parallel", notes1: comments_left, notes2: comments_right + = render "projects/notes/diff_notes_with_reply_parallel", notes_left: comments_left, notes_right: comments_right - if diff_file.diff.diff.blank? && diff_file.mode_changed? .file-mode-changed diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 20e51d18da5..1625930615a 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -1,17 +1,14 @@ .js-toggle-container .commit-stat-summary Showing - %strong.cdark #{pluralize(diffs.count, "changed file")} + = link_to '#', class: 'js-toggle-button' do + %strong #{pluralize(diffs.count, "changed file")} - if current_controller?(:commit) - unless @commit.has_zero_stats? with %strong.cgreen #{@commit.stats.additions} additions and %strong.cred #{@commit.stats.deletions} deletions - - = link_to '#', class: 'btn btn-small js-toggle-button' do - Show diff stats - %i.fa.fa-chevron-down .file-stats.js-toggle-content.hide %ul.bordered-list - diffs.each_with_index do |diff, i| @@ -26,7 +23,7 @@ %a{href: "#diff-#{i}"} %i.fa.fa-minus = diff.old_path - \-> + → = diff.new_path - elsif diff.new_file %span.new-file diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index b1c987563f1..a6373181b45 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -12,24 +12,24 @@ %tr.line_holder{ id: line_code, class: "#{type}" } - if type == "match" = render "projects/diffs/match_line", {line: line.text, - line_old: line_old, line_new: line.new_pos, bottom: false} + line_old: line_old, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file} - else %td.old_line = link_to raw(type == "new" ? " " : line_old), "##{line_code}", id: line_code - - if @comments_allowed + - if @comments_allowed && can?(current_user, :write_note, @project) = link_to_new_diff_note(line_code) %td.new_line{data: {linenumber: line.new_pos}} = link_to raw(type == "old" ? " " : line.new_pos) , "##{line_code}", id: line_code %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text) - if @reply_allowed - - comments = @line_notes.select { |n| n.line_code == line_code }.sort_by(&:created_at) + - 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: line.text - if last_line > 0 = render "projects/diffs/match_line", {line: "", - line_old: last_line, line_new: last_line, bottom: true} + line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file} - if diff_file.diff.blank? && diff_file.mode_changed? .file-mode-changed diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index 86ed6bbeaa2..bd0b7376ba7 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -1,19 +1,19 @@ -.bs-callout.bs-callout-warning +.alert.alert-warning %h4 Too many changes. .pull-right - unless diff_hard_limit_enabled? - = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true)), class: "btn btn-small btn-warning" + = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true)), class: "btn btn-sm btn-warning" - if current_controller?(:commit) or current_controller?(:merge_requests) - if current_controller?(:commit) - = link_to "Plain diff", project_commit_path(@project, @commit, format: :diff), class: "btn btn-warning btn-small" - = link_to "Email patch", project_commit_path(@project, @commit, format: :patch), class: "btn btn-warning btn-small" + = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-warning btn-sm" + = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-warning btn-sm" - elsif @merge_request && @merge_request.persisted? - = link_to "Plain diff", project_merge_request_path(@project, @merge_request, format: :diff), class: "btn btn-warning btn-small" - = link_to "Email patch", project_merge_request_path(@project, @merge_request, format: :patch), class: "btn btn-warning btn-small" + = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-warning btn-sm" + = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-warning btn-sm" %p To preserve performance only - %strong #{allowed_diff_size} of #{diffs.size} - files displayed. + %strong #{shown_files_count} of #{diffs.size} + files are displayed. diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index b85cf7d8d37..3fecd25c324 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -3,11 +3,11 @@ .project-edit-content %div %h3.page-title - Project settings: - %p.light Some settings, such as "Transfer Project", are hidden inside the danger area below. + Project settings %hr .panel-body - = form_for @project, remote: true, html: { class: "edit_project form-horizontal" } do |f| + = 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 @@ -29,57 +29,78 @@ .col-sm-10= f.select(:default_branch, @repository.branch_names, {}, {class: 'select2 select-wide'}) - = render "visibility_level", f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can?(current_user, :change_visibility_level, @project) + = render 'shared/visibility_level', f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can?(current_user, :change_visibility_level, @project), form_model: @project - %fieldset.features - %legend - Tags: - .form-group - = f.label :tag_list, "Tags", class: 'control-label' - .col-sm-10 - = f.text_field :tag_list, maxlength: 2000, class: "form-control" - %p.hint Separate tags with commas. + .form-group + = f.label :tag_list, "Tags", class: 'control-label' + .col-sm-10 + = f.text_field :tag_list, maxlength: 2000, class: "form-control" + %p.help-block Separate tags with commas. %fieldset.features %legend Features: .form-group - = f.label :issues_enabled, "Issues", class: 'control-label' - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :issues_enabled - %span.descr Lightweight issue tracking system for this project - - - if Project.issues_tracker.values.count > 1 - .form-group - = f.label :issues_tracker, "Issues tracker", class: 'control-label' - .col-sm-10= f.select(:issues_tracker, project_issues_trackers(@project.issues_tracker), {}, { disabled: !@project.issues_enabled }) - - .form-group - = f.label :issues_tracker_id, "Project name or id in issues tracker", class: 'control-label' - .col-sm-10= f.text_field :issues_tracker_id, disabled: !@project.can_have_issues_tracker_id?, class: 'form-control' + = f.label :issues_enabled do + = f.check_box :issues_enabled + %strong Issues + %br + %span.descr Lightweight issue tracking system for this project .form-group - = f.label :merge_requests_enabled, "Merge Requests", class: 'control-label' - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :merge_requests_enabled - %span.descr Submit changes to be merged upstream. + = 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 - = f.label :wiki_enabled, "Wiki", class: 'control-label' - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :wiki_enabled - %span.descr Pages for project documentation + = f.label :wiki_enabled do + = f.check_box :wiki_enabled + %strong Wiki + %br + %span.descr Pages for project documentation .form-group - = f.label :snippets_enabled, "Snippets", class: 'control-label' - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :snippets_enabled - %span.descr Share code pastes with others out of git repository + = f.label :snippets_enabled do + = f.check_box :snippets_enabled + %strong Snippets + %br + %span.descr Share code pastes with others out of git repository + %fieldset.features + %legend + Project avatar: + .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" @@ -99,7 +120,7 @@ The project can be committed to. %br %strong Once active this project shows up in the search and on the dashboard. - = link_to 'Unarchive', unarchive_project_path(@project), + = link_to 'Unarchive', 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 @@ -113,7 +134,7 @@ It is hidden from the dashboard and doesn't show up in searches. %br %strong Archived projects cannot be committed to! - = link_to 'Archive', archive_project_path(@project), + = link_to 'Archive', 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 @@ -123,7 +144,7 @@ .panel-heading Rename repository .errors-holder .panel-body - = form_for(@project, html: { class: 'form-horizontal' }) do |f| + = 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 @@ -136,6 +157,8 @@ .col-sm-9 .form-group .input-group + .input-group-addon + #{URI.join(root_url, @project.namespace.path)}/ = f.text_field :path, class: 'form-control' %span.input-group-addon .git %ul @@ -149,13 +172,13 @@ .panel-heading Transfer project .errors-holder .panel-body - = form_for(@project, url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| + = 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 - = f.label :namespace_id, class: 'control-label' do + = label_tag :new_namespace_id, nil, class: 'control-label' do %span Namespace .col-sm-10 .form-group - = f.select :namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace' }, { class: 'select2' } + = 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. @@ -169,7 +192,7 @@ .panel.panel-default.panel.panel-danger .panel-heading Remove project .panel-body - = form_tag(project_path(@project), method: :delete, html: { class: 'form-horizontal'}) do + = form_tag(namespace_project_path(@project.namespace, @project), method: :delete, html: { class: 'form-horizontal'}) do %p Removing the project will delete its repository and all related resources including issues, merge requests etc. %br diff --git a/app/views/projects/edit_tree/show.html.haml b/app/views/projects/edit_tree/show.html.haml deleted file mode 100644 index 5ccde05063e..00000000000 --- a/app/views/projects/edit_tree/show.html.haml +++ /dev/null @@ -1,72 +0,0 @@ -.file-editor - %ul.nav.nav-tabs.js-edit-mode - %li.active - = link_to 'Edit', '#editor' - %li - = link_to editing_preview_title(@blob.name), '#preview', 'data-preview-url' => preview_project_edit_tree_path(@project, @id) - - = form_tag(project_edit_tree_path(@project, @id), method: :put, class: "form-horizontal") do - .file-holder.file - .file-title - %i.fa.fa-file - %span.file_name - %span.monospace.light #{@ref}: - = @path - %span.options - .btn-group.tree-btn-group - = link_to "Cancel", @after_edit_path, class: "btn btn-tiny btn-cancel", data: { confirm: leave_edit_message } - .file-content.code - %pre.js-edit-mode-pane#editor - .js-edit-mode-pane#preview.hide - .center - %h2 - %i.fa.fa-spinner.fa-spin - = render 'shared/commit_message_container', params: params, - placeholder: "Update #{@blob.name}" - = hidden_field_tag 'last_commit', @last_commit - = hidden_field_tag 'content', '', id: "file-content" - = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] - = render 'projects/commit_button', ref: @ref, - cancel_path: @after_edit_path - -:javascript - ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace") - ace.config.loadModule("ace/ext/searchbox"); - var ace_mode = "#{@blob.language.try(:ace_mode)}"; - var editor = ace.edit("editor"); - editor.setValue("#{escape_javascript(@blob.data)}"); - if (ace_mode) { - editor.getSession().setMode('ace/mode/' + ace_mode); - } - - disableButtonIfEmptyField("#commit_message", ".js-commit-button"); - - $(".js-commit-button").click(function(){ - $("#file-content").val(editor.getValue()); - $(".file-editor form").submit(); - }); - - var editModePanes = $('.js-edit-mode-pane'), - editModeLinks = $('.js-edit-mode a'); - - editModeLinks.click(function(event) { - event.preventDefault(); - - var currentLink = $(this), - paneId = currentLink.attr('href'), - currentPane = editModePanes.filter(paneId); - - editModeLinks.parent().removeClass('active hover'); - currentLink.parent().addClass('active hover'); - editModePanes.hide(); - - if (paneId == '#preview') { - currentPane.fadeIn(200); - $.post(currentLink.data('preview-url'), { content: editor.getValue() }, function(response) { - currentPane.empty().append(response); - }) - } else { - currentPane.fadeIn(200); - editor.focus() - } - }) diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 59f19c8b7a3..8080a904978 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,5 +1,22 @@ +- if current_user && can?(current_user, :download_code, @project) + = render 'shared/no_ssh' + = render 'shared/no_password' + = render "home_panel" +.center.well + %h3 + The repository for this project is empty + %h4 + You can + = link_to namespace_project_new_blob_path(@project.namespace, @project, 'master'), class: 'btn btn-new btn-lg' do + add a file + or do a push via the command line. + +.well + = render "shared/clone_panel" +%h4 + %strong Command line instructions %div.git-empty %fieldset %legend Git global setup @@ -12,23 +29,22 @@ %legend Create a new repository %pre.dark :preserve - mkdir #{@project.path} + git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} cd #{@project.path} - git init touch README.md git add README.md - git commit -m "first commit" - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git commit -m "add README" git push -u origin master %fieldset - %legend Push an existing Git repository + %legend Existing folder or Git repository %pre.dark :preserve - cd existing_git_repo + cd existing_folder + git init git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git push -u origin master - if can? current_user, :remove_project, @project .prepend-top-20 - = link_to 'Remove project', @project, data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" + = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml index 76d3aa5bf00..3d0ab5b85d6 100644 --- a/app/views/projects/forks/error.html.haml +++ b/app/views/projects/forks/error.html.haml @@ -1,3 +1,4 @@ +- page_title "Fork project" - if @forked_project && !@forked_project.saved? .alert.alert-danger.alert-block %h4 @@ -15,6 +16,6 @@ = @forked_project.errors.full_messages.first %p - = link_to new_project_fork_path(@project), title: "Fork", class: "btn" do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork", class: "btn" do %i.fa.fa-code-fork Try to Fork again diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index 54f2cef023b..b7a2ed68e25 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -1,5 +1,7 @@ +- page_title "Fork project" %h3.page-title Fork project -%p.lead Select namespace where to fork this project +%p.lead + Click to fork the project to a user or group %hr .fork-namespaces @@ -17,7 +19,7 @@ = namespace.path - else .thumbnail.fork-thumbnail - = link_to project_fork_path(@project, namespace_id: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do + = link_to namespace_project_fork_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do = image_tag namespace_icon(namespace, 200) .caption %h4=namespace.human_name diff --git a/app/views/projects/go_import.html.haml b/app/views/projects/go_import.html.haml new file mode 100644 index 00000000000..87ac75a350f --- /dev/null +++ b/app/views/projects/go_import.html.haml @@ -0,0 +1,5 @@ +!!! 5 +%html + %head + - web_url = [Gitlab.config.gitlab.url, @namespace, @id].join('/') + %meta{name: "go-import", content: "#{web_url.split('://')[1]} git #{web_url}.git"} diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 9f37a760e61..9383df13305 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,5 +1,5 @@ %ul.nav.nav-tabs = nav_link(action: :show) do - = link_to 'Contributors', project_graph_path + = link_to 'Contributors', namespace_project_graph_path = nav_link(action: :commits) do - = link_to 'Commits', commits_project_graph_path + = link_to 'Commits', commits_namespace_project_graph_path diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index a189a487135..254a76e108b 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -1,7 +1,8 @@ +- page_title "Commit statistics" = render 'head' %p.lead - Commits statistic for + Commit statistics for %strong #{@repository.root_ref} #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} @@ -54,7 +55,7 @@ } ctx = $("#hour-chart").get(0).getContext("2d"); - new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true}); + new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2}) data = { labels : #{@commits_per_week_days.keys.to_json}, @@ -68,7 +69,7 @@ } ctx = $("#weekday-chart").get(0).getContext("2d"); - new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true}); + new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2}) data = { labels : #{@commits_per_month.keys.to_json}, @@ -82,4 +83,4 @@ } ctx = $("#month-chart").get(0).getContext("2d"); - new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true}); + new Chart(ctx).Line(data, {"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2}) diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index e3d5094ddc5..3a8dc89f84c 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,3 +1,4 @@ +- page_title "Contributor statistics" = render 'head' .loading-graph .center diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 9a003c87f68..eadbf61fdd4 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Web Hooks" %h3.page-title Web hooks @@ -7,7 +8,7 @@ %hr.clearfix -= form_for [@project, @hook], as: :hook, url: project_hooks_path(@project), html: { class: 'form-horizontal' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| -if @hook.errors.any? .alert.alert-danger - @hook.errors.full_messages.each do |msg| @@ -34,6 +35,13 @@ %p.light This url will be triggered when a new tag is pushed to the repository %div + = f.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = f.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This url will be triggered when someone adds a comment + %div = f.check_box :issues_events, class: 'pull-left' .prepend-left-20 = f.label :issues_events, class: 'list-label' do @@ -58,11 +66,11 @@ - @hooks.each do |hook| %li .pull-right - = link_to 'Test Hook', test_project_hook_path(@project, hook), class: "btn btn-small btn-grouped" - = link_to 'Remove', project_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-small btn-grouped" + = link_to 'Test Hook', test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped" + = link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" .clearfix %span.monospace= hook.url %p - - %w(push_events tag_push_events issues_events merge_requests_events).each do |trigger| + - %w(push_events tag_push_events issues_events note_events merge_requests_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray= trigger.titleize diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index 6c3083e49f5..f8f2e192e29 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -1,3 +1,4 @@ +- page_title "Import repository" %h3.page-title - if @project.import_failed? Import failed. Retry? @@ -6,13 +7,13 @@ %hr -= form_for @project, url: project_import_path(@project), method: :post, html: { class: 'form-horizontal' } do |f| += form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f| .form-group.import-url-data = f.label :import_url, class: 'control-label' do %span Import existing git repo .col-sm-10 = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' - .bs-callout.bs-callout-info + .well.prepend-top-20 This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. %br The import will time out after 4 minutes. For big repositories, use a clone/push combination. diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml index 2d1fdafed24..39fe0fc1c4f 100644 --- a/app/views/projects/imports/show.html.haml +++ b/app/views/projects/imports/show.html.haml @@ -1,3 +1,4 @@ +- page_title "Import in progress" .save-project-loader .center %h2 diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml new file mode 100644 index 00000000000..48858fa32da --- /dev/null +++ b/app/views/projects/issues/_discussion.html.haml @@ -0,0 +1,33 @@ +- content_for :note_actions do + - if can?(current_user, :modify_issue, @issue) + - if @issue.closed? + = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue' + - else + = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close js-note-target-close', title: 'Close Issue' + += render 'shared/show_aside' + +.row + %section.col-md-9 + .votes-holder.pull-right + #votes= render 'votes/votes_block', votable: @issue + .participants + %span= pluralize(@issue.participants(current_user).count, 'participant') + - @issue.participants(current_user).each do |participant| + = link_to_member(@project, participant, name: false, size: 24) + .voting_notes#notes= render 'projects/notes/notes_with_form' + %aside.col-md-3 + .issuable-affix + .clearfix + %span.slead.has_tooltip{title: 'Cross-project reference'} + = cross_project_reference(@project, @issue) + %hr + .context + = render partial: 'issue_context', locals: { issue: @issue } + + - if @issue.labels.any? + .issuable-context-title + %label Labels + .issue-show-labels + - @issue.labels.each do |label| + = link_to_label(label) diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 64a28d8da49..8d2564be55e 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,14 +1,8 @@ %div.issue-form-holder - %h3.page-title= @issue.new_record? ? "New Issue" : "Edit Issue ##{@issue.iid}" + %h3.page-title= @issue.new_record? ? "Create Issue" : "Edit Issue ##{@issue.iid}" %hr - - if @repository.exists? && !@repository.empty? && @repository.contribution_guide && !@issue.persisted? - - contribution_guide_url = project_blob_path(@project, tree_join(@repository.root_ref, @repository.contribution_guide.name)) - .row - .col-sm-10.col-sm-offset-2 - .alert.alert-info - = "Please review the <strong>#{link_to "guidelines for contribution", contribution_guide_url}</strong> to this repository.".html_safe - = form_for [@project, @issue], html: { class: 'form-horizontal issue-form gfm-form' } do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form' } do |f| = render 'projects/issuable_form', f: f, issuable: @issue :javascript @@ -16,5 +10,3 @@ $('#issue_assignee_id').val("#{current_user.id}").trigger("change"); e.preventDefault(); }); - - window.project_image_path_upload = "#{upload_image_project_path @project}"; diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 85a3d2b6c01..2c296cab977 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,26 +1,38 @@ -%li{ id: dom_id(issue), class: issue_css_classes(issue), url: project_issue_path(issue.project, issue) } +%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue) } - if controller.controller_name == 'issues' .issue-check = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue", disabled: !can?(current_user, :modify_issue, issue) .issue-title - %span.light= "##{issue.iid}" - %span.str-truncated - = link_to_gfm issue.title, project_issue_path(issue.project, issue), class: "row_title" - - if issue.closed? - %small.pull-right - CLOSED + %span.issue-title-text + = link_to_gfm issue.title, issue_path(issue), class: "row_title" + .issue-labels + - issue.labels.each do |label| + = link_to_label(label, project: issue.project) + .pull-right.light + - if issue.closed? + %span + CLOSED + - if issue.assignee + = link_to_member(@project, issue.assignee, name: false) + - note_count = issue.notes.user.count + - if note_count > 0 + + %span + %i.fa.fa-comments + = note_count + - else + + %span.issue-no-comments + %i.fa.fa-comments + = 0 .issue-info - - if issue.assignee - assigned to #{link_to_member(@project, issue.assignee)} + = "##{issue.iid} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe - if issue.votes_count > 0 = render 'votes/votes_inline', votable: issue - - if issue.notes.any? - %span - %i.fa.fa-comments - = issue.notes.count - if issue.milestone + %span %i.fa.fa-clock-o = issue.milestone.title @@ -29,21 +41,4 @@ = issue.task_status .pull-right.issue-updated-at - %small updated #{time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_update_ago')} - - .issue-labels - - issue.labels.each do |label| - = link_to project_issues_path(issue.project, label_name: label.name) do - = render_colored_label(label) - - .issue-actions - - if can? current_user, :modify_issue, issue - - if issue.closed? - = link_to 'Reopen', project_issue_path(issue.project, issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-small btn-grouped reopen_issue btn-reopen", remote: true - - else - = link_to 'Close', project_issue_path(issue.project, issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-small btn-grouped close_issue btn-close", remote: true - = link_to edit_project_issue_path(issue.project, issue), class: "btn btn-small edit-issue-link btn-grouped" do - %i.fa.fa-pencil-square-o - Edit - - + %small updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')} diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index 648f459dc9e..323f5c84a85 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -1,25 +1,46 @@ -= form_for [@project, @issue], remote: true, html: {class: 'edit-issue inline-update'} do |f| - .row - .col-sm-6 - %strong.append-right-10 += form_for [@project.namespace.becomes(Namespace), @project, @issue], remote: true, html: {class: 'edit-issue inline-update js-issue-update'} do |f| + %div.prepend-top-20 + .issuable-context-title + %label Assignee: - - - if can?(current_user, :modify_issue, @issue) - = project_users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @issue.assignee_id) - - elsif issue.assignee - = link_to_member(@project, @issue.assignee) + - if issue.assignee + %strong= link_to_member(@project, @issue.assignee, size: 24) - else - None + none + - if can?(current_user, :modify_issue, @issue) + = users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @issue.assignee_id, null_user: true, first_user: true) - .col-sm-6.text-right - %strong.append-right-10 + %div.prepend-top-20.clearfix + .issuable-context-title + %label Milestone: - - if can?(current_user, :modify_issue, @issue) - = f.select(:milestone_id, milestone_options(@issue), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'}) - = hidden_field_tag :issue_context - = f.submit class: 'btn' - - elsif issue.milestone - = link_to project_milestone_path(@project, @issue.milestone) do - = @issue.milestone.title + - if issue.milestone + %span.back-to-milestone + = link_to namespace_project_milestone_path(@project.namespace, @project, @issue.milestone) do + %strong + %i.fa.fa-clock-o + = @issue.milestone.title - else - None + none + - if can?(current_user, :modify_issue, @issue) + = f.select(:milestone_id, milestone_options(@issue), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'}) + = hidden_field_tag :issue_context + = f.submit class: 'btn' + + - if current_user + %div.prepend-top-20.clearfix + .issuable-context-title + %label + Subscription: + %button.btn.btn-block.subscribe-button{:type => 'button'} + %i.fa.fa-eye + %span= @issue.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" + - subscribtion_status = @issue.subscribed?(current_user) ? "subscribed" : "unsubscribed" + .subscription-status{"data-status" => subscribtion_status} + .description-block.unsubscribed{class: ( "hidden" if @issue.subscribed?(current_user) )} + You're not receiving notifications from this thread. + .description-block.subscribed{class: ( "hidden" unless @issue.subscribed?(current_user) )} + You're receiving notifications because you're subscribed to this thread. + +:coffeescript + new Subscription("#{toggle_subscription_namespace_project_issue_path(@issue.project.namespace, @project, @issue)}") diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index 0bff8bdbead..5d243adb5fe 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -1,66 +1,3 @@ -.append-bottom-10 - .check-all-holder - = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" - .issues-filters - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-user - %span.light assignee: - - if @assignee.present? - %strong= @assignee.name - - elsif params[:assignee_id] == "0" - Unassigned - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(assignee_id: nil) do - Any - = link_to project_filter_path(assignee_id: 0) do - Unassigned - - @assignees.sort_by(&:name).each do |user| - %li - = link_to project_filter_path(assignee_id: user.id) do - = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' - = user.name - - .dropdown.inline.prepend-left-10 - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-clock-o - %span.light milestone: - - if @milestone.present? - %strong= @milestone.title - - elsif params[:milestone_id] == "0" - None (backlog) - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(milestone_id: nil) do - Any - = link_to project_filter_path(milestone_id: 0) do - None (backlog) - - project_active_milestones.each do |milestone| - %li - = link_to project_filter_path(milestone_id: milestone.id) do - %strong= milestone.title - %small.light= milestone.expires_at - - .pull-right - = render 'shared/sort_dropdown' - - .clearfix - .issues_bulk_update.hide - = form_tag bulk_update_project_issues_path(@project), method: :post do - = select_tag('update[status]', options_for_select([['Open', 'open'], ['Closed', 'closed']]), prompt: "Status") - = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee') - = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") - = hidden_field_tag 'update[issues_ids]', [] - = hidden_field_tag :status, params[:status] - = button_tag "Update issues", class: "btn update_selected_issues btn-save" - .panel.panel-default %ul.well-list.issues-list = render @issues diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml index b1bc3ba0eba..53b6f0879c9 100644 --- a/app/views/projects/issues/edit.html.haml +++ b/app/views/projects/issues/edit.html.haml @@ -1 +1,2 @@ +- page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues" = render "form" diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index 012ba235951..dc8e477185b 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -1,23 +1,12 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@project.name} issues" - xml.link :href => project_issues_url(@project, :atom), :rel => "self", :type => "application/atom+xml" - xml.link :href => project_issues_url(@project), :rel => "alternate", :type => "text/html" - xml.id project_issues_url(@project) + xml.link href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html" + xml.id namespace_project_issues_url(@project.namespace, @project) xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? @issues.each do |issue| - xml.entry do - xml.id project_issue_url(@project, issue) - xml.link :href => project_issue_url(@project, issue) - xml.title truncate(issue.title, :length => 80) - xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, :width => "40", :height => "40", :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 + issue_to_atom(xml, issue) end end diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 8db6241f21f..1d5597602d1 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,9 +1,24 @@ -= render "projects/issues_nav" -.row - .fixed.fixed.sidebar-expand-button.hidden-lg.hidden-md.hidden-xs - %i.fa.fa-list.fa-2x - .col-md-3.responsive-side - = render 'shared/project_filter', project_entities_path: project_issues_path(@project), - labels: true, redirect: 'issues', entity: 'issue' - .col-md-9.issues-holder - = render "issues" +- page_title "Issues" += 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") + +.append-bottom-10 + .pull-right + .pull-left + - if current_user + .hidden-xs.pull-left + = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do + %i.fa.fa-rss + + = render 'shared/issuable_search_form', path: namespace_project_issues_path(@project.namespace, @project) + + - if can? current_user, :write_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 pull-left", title: "New Issue", id: "new_issue_link" do + %i.fa.fa-plus + New Issue + + = render 'shared/issuable_filter', type: :issues + +.issues-holder + = render "issues" diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml index b1bc3ba0eba..da6edd5c2d2 100644 --- a/app/views/projects/issues/new.html.haml +++ b/app/views/projects/issues/new.html.haml @@ -1 +1,2 @@ +- page_title "New Issue" = render "form" diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index aad58e48f6c..ee1b2a08bc4 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,75 +1,43 @@ -%h3.page-title - Issue ##{@issue.iid} - - %span.pull-right.issue-btn-group - - if can?(current_user, :write_issue, @project) - = link_to new_project_issue_path(@project), class: "btn btn-grouped", title: "New Issue", id: "new_issue_link" do - %i.fa.fa-plus - New Issue - - if can?(current_user, :modify_issue, @issue) - - if @issue.closed? - = link_to 'Reopen', project_issue_path(@project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen" - - else - = link_to 'Close', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close", title: "Close Issue" - - = link_to edit_project_issue_path(@project, @issue), class: "btn btn-grouped" do - %i.fa.fa-pencil-square-o - Edit - -.clearfix - .votes-holder - #votes= render 'votes/votes_block', votable: @issue - - .back-link - = link_to project_issues_path(@project) do - ← To issues list - %span.milestone-nav-link - - if @issue.milestone - | - %span.light Milestone - = link_to project_milestone_path(@project, @issue.milestone) do - = @issue.milestone.title - -.issue-box{ class: issue_box_class(@issue) } - .state.clearfix - .state-label - - if @issue.closed? - Closed - - else - Open - - .creator - Created by #{link_to_member(@project, @issue.author)} #{issue_timestamp(@issue)} - - %h4.title - = gfm escape_once(@issue.title) - - - if @issue.description.present? - .description - .wiki - = preserve do - = markdown(@issue.description, parse_tasks: true) - .context - %cite.cgray - = render partial: 'issue_context', locals: { issue: @issue } - - -- content_for :note_actions do - - if can?(current_user, :modify_issue, @issue) - - if @issue.closed? - = link_to 'Reopen Issue', project_issue_path(@project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen js-note-target-reopen", title: 'Reopen Issue' - - else - = link_to 'Close Issue', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue" - -.participants - %cite.cgray - = pluralize(@issue.participants.count, 'participant') - - @issue.participants.each do |participant| - = link_to_member(@project, participant, name: false, size: 24) - - .issue-show-labels.pull-right - - @issue.labels.each do |label| - = link_to project_issues_path(@project, label_name: label.name) do - = render_colored_label(label) - -.voting_notes#notes= render "projects/notes/notes_with_form" +- page_title "#{@issue.title} (##{@issue.iid})", "Issues" +.issue + .issue-details + %h4.page-title + .issue-box{ class: issue_box_class(@issue) } + - if @issue.closed? + Closed + - else + Open + Issue ##{@issue.iid} + %small.creator + · created by #{link_to_member(@project, @issue.author)} #{issue_timestamp(@issue)} + + .pull-right + - if can?(current_user, :write_issue, @project) + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-grouped new-issue-link', title: 'New Issue', id: 'new_issue_link' do + = icon('plus') + New Issue + - if can?(current_user, :modify_issue, @issue) + - if @issue.closed? + = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen' + - else + = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close', title: 'Close Issue' + + = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-grouped issuable-edit' do + = icon('pencil-square-o') + Edit + + %hr + %h2.issue-title + = gfm escape_once(@issue.title) + %div + - if @issue.description.present? + .description{class: can?(current_user, :modify_issue, @issue) ? 'js-task-list-container' : ''} + .wiki + = preserve do + = markdown(@issue.description) + %textarea.hidden.js-task-list-field + = @issue.description + + %hr + .issue-discussion + = render 'projects/issues/discussion' diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml index 5199e9fc61f..1d38662bff8 100644 --- a/app/views/projects/issues/update.js.haml +++ b/app/views/projects/issues/update.js.haml @@ -3,8 +3,15 @@ :plain $("##{dom_id(@issue)}").fadeOut(); - elsif params[:issue_context] - $('.issue-box .context').effect('highlight'); + $('.context').html("#{escape_javascript(render partial: 'issue_context', locals: { issue: @issue })}"); + $('.context').effect('highlight'); - if @issue.milestone - $('.milestone-nav-link').replaceWith("<span class='milestone-nav-link'>| <span class='light'>Milestone</span> #{escape_javascript(link_to @issue.milestone.title, project_milestone_path(@issue.project, @issue.milestone))}</span>") + $('.milestone-nav-link').replaceWith("<span class='milestone-nav-link'>| <span class='light'>Milestone</span> #{escape_javascript(link_to @issue.milestone.title, namespace_project_milestone_path(@issue.project.namespace, @issue.project, @issue.milestone))}</span>") - else $('.milestone-nav-link').html('') + + +$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) +$('.edit-issue.inline-update input[type="submit"]').hide(); +new UsersSelect() +new Issue(); diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 72a01e1c271..d791ed3410c 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -1,8 +1,8 @@ -= form_for [@project, @label], html: { class: 'form-horizontal label-form' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form' } do |f| -if @label.errors.any? .row - .col-sm-10.col-sm-offset-2 - .bs-callout.bs-callout-danger + .col-sm-offset-2.col-sm-10 + .alert.alert-danger - @label.errors.full_messages.each do |msg| %span= msg %br @@ -16,9 +16,9 @@ .col-sm-10 .input-group .input-group-addon.label-color-preview - = f.text_field :color, placeholder: "#AA33EE", class: "form-control" + = f.color_field :color, class: "form-control" .help-block - 6 character hex values starting with a # sign. + Choose any color. %br Or you can choose one of suggested colors below @@ -29,5 +29,5 @@ .form-actions = f.submit 'Save', class: 'btn btn-save js-save-button' - = link_to "Cancel", project_labels_path(@project), class: 'btn btn-cancel' + = link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 03a8f0921b7..7fa1ee53f76 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,10 +1,10 @@ %li{id: dom_id(label)} - = render_colored_label(label) + = link_to_label(label) .pull-right %strong.append-right-20 - = link_to project_issues_path(@project, label_name: label.name) do + = link_to_label(label) do = pluralize label.open_issues_count, 'open issue' - if can? current_user, :admin_label, @project - = link_to 'Edit', edit_project_label_path(@project, label), class: 'btn' - = link_to 'Remove', project_label_path(@project, label), class: 'btn btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} + = link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn' + = link_to 'Remove', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index 52435c5d892..645402667fd 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -1,8 +1,9 @@ +- page_title "Edit", @label.name, "Labels" %h3 Edit label %span.light #{@label.name} .back-link - = link_to project_labels_path(@project) do + = link_to namespace_project_labels_path(@project.namespace, @project) do ← To labels list %hr = render 'form' diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index c7c17c7797e..d44fe486212 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,7 +1,6 @@ -= render "projects/issues_nav" - +- page_title "Labels" - if can? current_user, :admin_label, @project - = link_to new_project_label_path(@project), class: "pull-right btn btn-new" do + = link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do New label %h3.page-title Labels @@ -14,4 +13,7 @@ = paginate @labels, theme: 'gitlab' - else .light-well - .nothing-here-block Create first label or #{link_to 'generate', generate_project_labels_path(@project), method: :post} default set of labels + - if can? current_user, :admin_label, @project + .nothing-here-block Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels + - else + .nothing-here-block No labels created diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index 850da0b192b..b3ef17025c3 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -1,6 +1,7 @@ +- page_title "New Label" %h3 New label .back-link - = link_to project_labels_path(@project) do + = link_to namespace_project_labels_path(@project.namespace, @project) do ← To labels list %hr = render 'form' diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml new file mode 100644 index 00000000000..eb3dba6858d --- /dev/null +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -0,0 +1,30 @@ +- content_for :note_actions do + - if can?(current_user, :modify_merge_request, @merge_request) + - if @merge_request.open? + = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" + - if @merge_request.closed? + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" + += render 'shared/show_aside' + +.row + %section.col-md-9 + .votes-holder.pull-right + #votes= render 'votes/votes_block', votable: @merge_request + = render "projects/merge_requests/show/participants" + = render "projects/notes/notes_with_form" + %aside.col-md-3 + .issuable-affix + .clearfix + %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'} + = cross_project_reference(@project, @merge_request) + %hr + .context + = render partial: 'projects/merge_requests/show/context', locals: { merge_request: @merge_request } + + - if @merge_request.labels.any? + .issuable-context-title + %label Labels + .merge-request-show-labels + - @merge_request.labels.each do |label| + = link_to_label(label) diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index d52e64666a0..be73f087449 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form' } do |f| .merge-request-form-info = render 'projects/issuable_form', f: f, issuable: @merge_request @@ -8,5 +8,3 @@ $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); e.preventDefault(); }); - - window.project_image_path_upload = "#{upload_image_project_path @project}"; diff --git a/app/views/projects/merge_requests/_head.html.haml b/app/views/projects/merge_requests/_head.html.haml index 35a86e6511c..19e4dab874b 100644 --- a/app/views/projects/merge_requests/_head.html.haml +++ b/app/views/projects/merge_requests/_head.html.haml @@ -1,5 +1,5 @@ .top-tabs - = link_to project_merge_requests_path(@project), class: "tab #{'active' if current_page?(project_merge_requests_path(@project)) }" do + = 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/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 0a719fc642e..d79c4548247 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,29 +1,45 @@ %li{ class: mr_css_classes(merge_request) } .merge-request-title - %span.light= "##{merge_request.iid}" - = link_to_gfm truncate(merge_request.title, length: 80), project_merge_request_path(merge_request.target_project, merge_request), class: "row_title" - - if merge_request.merged? - %small.pull-right - %i.fa.fa-check - MERGED - - else - %span.pull-right.hidden-xs - - if merge_request.for_fork? - %span.light - #{merge_request.source_project_namespace}: - = truncate merge_request.source_branch, length: 25 - %i.fa.fa-angle-right.light - = merge_request.target_branch + %span.merge-request-title-text + = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" + .merge-request-labels + - merge_request.labels.each do |label| + = link_to_label(label, project: merge_request.project) + .pull-right.light + - if merge_request.merged? + %span + %i.fa.fa-check + ACCEPTED + - elsif merge_request.closed? + %span + %i.fa.fa-ban + REJECTED + - else + %span.hidden-xs.hidden-sm + %span.label-branch< + %i.fa.fa-code-fork + %span= merge_request.target_branch + - note_count = merge_request.mr_and_commit_notes.user.count + - if merge_request.assignee + + = link_to_member(merge_request.source_project, merge_request.assignee, name: false) + - if note_count > 0 + + %span + %i.fa.fa-comments + = note_count + - else + + %span.merge-request-no-comments + %i.fa.fa-comments + = 0 + .merge-request-info - - if merge_request.author - authored by #{link_to_member(merge_request.source_project, merge_request.author)} + = "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe - if merge_request.votes_count > 0 = render 'votes/votes_inline', votable: merge_request - - if merge_request.notes.any? - %span - %i.fa.fa-comments - = merge_request.mr_and_commit_notes.count - if merge_request.milestone_id? + %span %i.fa.fa-clock-o = merge_request.milestone.title @@ -32,9 +48,4 @@ = merge_request.task_status .pull-right.hidden-xs - %small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')} - - .merge-request-labels - - merge_request.labels.each do |label| - = link_to project_merge_requests_path(merge_request.project, label_name: label.name) do - = render_colored_label(label) + %small updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')} diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml new file mode 100644 index 00000000000..b8a0ca9a42f --- /dev/null +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -0,0 +1,13 @@ +.panel.panel-default + %ul.well-list.mr-list + = render @merge_requests + - if @merge_requests.blank? + %li + .nothing-here-block No merge requests to show + +- if @merge_requests.present? + .pull-right + %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter + + = paginate @merge_requests, theme: "gitlab" + diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 99726172154..e611b23bca6 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -1,7 +1,6 @@ -%h3.page-title Compare branches for new Merge Request -%hr +%p.lead Compare branches for new Merge Request -= form_for [@project, @merge_request], url: new_project_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline" } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: new_namespace_project_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline" } do |f| .hide.alert.alert-danger.mr-compare-errors .merge-request-branches.row .col-md-6 @@ -52,27 +51,27 @@ are the same. - %hr - = f.submit 'Compare branches', class: "btn btn-primary mr-compare-btn" + %div + = f.submit 'Compare branches', class: "btn btn-new mr-compare-btn" :javascript var source_branch = $("#merge_request_source_branch") , target_branch = $("#merge_request_target_branch") , target_project = $("#merge_request_target_project_id"); - $.get("#{branch_from_project_merge_requests_path(@source_project)}", {ref: source_branch.val() }); - $.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: target_branch.val() }); + $.get("#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {ref: source_branch.val() }); + $.get("#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: target_project.val(),ref: target_branch.val() }); target_project.on("change", function() { - $.get("#{update_branches_project_merge_requests_path(@source_project)}", {target_project_id: $(this).val() }); + $.get("#{update_branches_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: $(this).val() }); }); source_branch.on("change", function() { - $.get("#{branch_from_project_merge_requests_path(@source_project)}", {ref: $(this).val() }); + $.get("#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {ref: $(this).val() }); $(".mr-compare-errors").fadeOut(); $(".mr-compare-btn").enable(); }); target_branch.on("change", function() { - $.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: $(this).val() }); + $.get("#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: target_project.val(),ref: $(this).val() }); $(".mr-compare-errors").fadeOut(); $(".mr-compare-btn").enable(); }); diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index d4666eacd7e..6792104569b 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -7,75 +7,43 @@ %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch} %span.pull-right - = link_to 'Change branches', new_project_merge_request_path(@project) - -= form_for [@project, @merge_request], html: { class: "merge-request-form gfm-form" } do |f| - .panel.panel-default - - .panel-body - .form-group - .light - = f.label :title do - Title * - = f.text_field :title, class: "form-control input-lg js-gfm-input", maxlength: 255, rows: 5, required: true - .form-group - .light - = f.label :description, "Description" - = render 'projects/zen', f: f, attr: :description, - classes: 'description form-control' - .clearfix.hint - .pull-left Description is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. - .error-alert - .form-group - .issue-assignee - = f.label :assignee_id do - %i.fa.fa-user - Assign to - %div - = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @merge_request.assignee_id, project_id: @merge_request.target_project_id) - - = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' - .form-group - .issue-milestone - = f.label :milestone_id do - %i.fa.fa-clock-o - Milestone - %div= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2'}) - .form-group - = f.label :label_ids do - %i.fa.fa-tag - Labels - %div - = f.collection_select :label_ids, @merge_request.target_project.labels.all, :id, :name, { selected: @merge_request.label_ids }, multiple: true, class: 'select2' - - .panel-footer - - if contribution_guide_url(@target_project) - %p - Please review the - %strong #{link_to "guidelines for contribution", contribution_guide_url(@target_project)} - to this repository. - = f.hidden_field :source_project_id - = f.hidden_field :target_project_id - = f.hidden_field :target_branch - = f.hidden_field :source_branch - = f.submit 'Submit merge request', class: "btn btn-create" - -.mr-compare - = render "projects/commits/commit_list" - - %h4 Changes - - if @diffs.present? - = render "projects/diffs/diffs", diffs: @diffs, project: @project - - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - .bs-callout.bs-callout-danger - %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. - %p To preserve performance the line changes are not shown. - - else - .bs-callout.bs-callout-danger - %h4 This comparison includes huge diff. - %p To preserve performance the line changes are not shown. - + = link_to 'Change branches', mr_change_branches_path(@merge_request) +%hr += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form' } do |f| + .merge-request-form-info + = render 'projects/issuable_form', f: f, issuable: @merge_request + = f.hidden_field :source_project_id + = f.hidden_field :source_branch + = f.hidden_field :target_project_id + = f.hidden_field :target_branch + +.mr-compare.merge-request + %ul.nav.nav-tabs.merge-request-tabs + %li.commits-tab + = link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do + = icon('history') + Commits + %span.badge= @commits.size + %li.diffs-tab + = link_to url_for(params), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do + = icon('list-alt') + Changes + %span.badge= @diffs.size + + .tab-content + #commits.commits.tab-pane + = render "projects/commits/commits", project: @project + #diffs.diffs.tab-pane + - if @diffs.present? + = render "projects/diffs/diffs", diffs: @diffs, project: @project + - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE + .alert.alert-danger + %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. + %p To preserve performance the line changes are not shown. + - else + .alert.alert-danger + %h4 This comparison includes a huge diff. + %p To preserve performance the line changes are not shown. :javascript $('.assign-to-me-link').on('click', function(e){ @@ -83,4 +51,11 @@ e.preventDefault(); }); - window.project_image_path_upload = "#{upload_image_project_path @project}"; +:javascript + var merge_request + merge_request = new MergeRequest({ + action: 'new', + diffs_loaded: true, + commits_loaded: true + }); + diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 7b28dd5e7da..9dc4a47258e 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,36 +1,67 @@ -.merge-request - = render "projects/merge_requests/show/mr_title" - = render "projects/merge_requests/show/how_to_merge" - = render "projects/merge_requests/show/mr_box" - = render "projects/merge_requests/show/state_widget" - = render "projects/merge_requests/show/commits" - = render "projects/merge_requests/show/participants" +- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" +.merge-request{'data-url' => merge_request_path(@merge_request)} + .merge-request-details + = render "projects/merge_requests/show/mr_title" + %hr + = render "projects/merge_requests/show/mr_box" + %hr + .append-bottom-20 + .slead + %span From + - if @merge_request.for_fork? + %strong.label-branch< + - if @merge_request.source_project + = link_to @merge_request.source_project_namespace, namespace_project_path(@merge_request.source_project.namespace, @merge_request.source_project) + - else + \ #{@merge_request.source_project_namespace} + \:#{@merge_request.source_branch} + %span into + %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch} + - else + %strong.label-branch #{@merge_request.source_branch} + %span into + %strong.label-branch #{@merge_request.target_branch} + - if @merge_request.open? + .btn-group.btn-group-sm.pull-right + %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } + = icon('download') + Download as + %span.caret + %ul.dropdown-menu + %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) + %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) + + = render "projects/merge_requests/show/how_to_merge" + = render "projects/merge_requests/widget/show.html.haml" - if @commits.present? - %ul.nav.nav-pills.merge-request-tabs - %li.notes-tab{data: {action: 'notes'}} - = link_to project_merge_request_path(@project, @merge_request) do - %i.fa.fa-comment + %ul.nav.nav-tabs.merge-request-tabs + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#notes', action: 'notes', toggle: 'tab'} do + = icon('comments') Discussion - %span.badge= @merge_request.mr_and_commit_notes.count - %li.diffs-tab{data: {action: 'diffs'}} - = link_to diffs_project_merge_request_path(@project, @merge_request) do - %i.fa.fa-list-alt + %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: '#commits', action: 'commits', toggle: 'tab'} do + = icon('history') + Commits + %span.badge= @commits.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do + = icon('list-alt') Changes %span.badge= @merge_request.diffs.size - - content_for :note_actions do - - if can?(current_user, :modify_merge_request, @merge_request) - - if @merge_request.open? - = link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" - - if @merge_request.closed? - = link_to 'Reopen', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" + .tab-content + #notes.notes.tab-pane.voting_notes + = render "projects/merge_requests/discussion" + #commits.commits.tab-pane + - if current_page?(action: 'commits') + = render "projects/merge_requests/show/commits" + #diffs.diffs.tab-pane + - if current_page?(action: 'diffs') + = render "projects/merge_requests/show/diffs" - .diffs.tab-content - - if current_page?(action: 'diffs') - = render "projects/merge_requests/show/diffs" - .notes.tab-content.voting_notes#notes{ class: (controller.action_name == 'show') ? "" : "hide" } - = render "projects/notes/notes_with_form" .mr-loading-status = spinner @@ -38,10 +69,5 @@ var merge_request; merge_request = new MergeRequest({ - url_to_automerge_check: "#{automerge_check_project_merge_request_path(@project, @merge_request)}", - check_enable: #{@merge_request.unchecked? ? "true" : "false"}, - url_to_ci_check: "#{ci_status_project_merge_request_path(@project, @merge_request)}", - ci_enable: #{@project.ci_service ? "true" : "false"}, - current_status: "#{@merge_request.merge_status_name}", action: "#{controller.action_name}" }); diff --git a/app/views/projects/merge_requests/automerge.js.haml b/app/views/projects/merge_requests/automerge.js.haml index e01ff662e7d..33321651e32 100644 --- a/app/views/projects/merge_requests/automerge.js.haml +++ b/app/views/projects/merge_requests/automerge.js.haml @@ -1,7 +1,6 @@ --if @status +- if @status :plain - location.reload(); --else + merge_request_widget.mergeInProgress(); +- else :plain - merge_request.alreadyOrCannotBeMerged() - + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}"); diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index 839c63986ab..7e5cb07f249 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "Edit", "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" %h3.page-title = "Edit merge request ##{@merge_request.iid}" %hr diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index a6d90a68b11..fa591b0537e 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,71 +1,13 @@ -= render "projects/issues_nav" - -.row - .col-md-3.responsive-side - = render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project), - labels: true, redirect: 'merge_requests', entity: 'merge_request' - .col-md-9 - .mr-filters.append-bottom-10 - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-user - %span.light assignee: - - if @assignee.present? - %strong= @assignee.name - - elsif params[:assignee_id] == "0" - Unassigned - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(assignee_id: nil) do - Any - = link_to project_filter_path(assignee_id: 0) do - Unassigned - - @assignees.sort_by(&:name).each do |user| - %li - = link_to project_filter_path(assignee_id: user.id) do - = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' - = user.name - - .dropdown.inline.prepend-left-10 - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-clock-o - %span.light milestone: - - if @milestone.present? - %strong= @milestone.title - - elsif params[:milestone_id] == "0" - None (backlog) - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(milestone_id: nil) do - Any - = link_to project_filter_path(milestone_id: 0) do - None (backlog) - - project_active_milestones.each do |milestone| - %li - = link_to project_filter_path(milestone_id: milestone.id) do - %strong= milestone.title - %small.light= milestone.expires_at - - .pull-right - = render 'shared/sort_dropdown' - - .panel.panel-default - %ul.well-list.mr-list - = render @merge_requests - - if @merge_requests.blank? - %li - .nothing-here-block No merge requests to show - - if @merge_requests.present? - .pull-right - %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter - - = paginate @merge_requests, theme: "gitlab" - -:javascript - $(merge_requestsPage); +- page_title "Merge Requests" +.append-bottom-10 + .pull-right + = render 'shared/issuable_search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) + + - if can? current_user, :write_merge_request, @project + .pull-left.hidden-xs + = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new", title: "New Merge Request" do + %i.fa.fa-plus + New Merge Request + = render 'shared/issuable_filter', type: :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 b9c466657de..15bd4e2fafd 100644 --- a/app/views/projects/merge_requests/invalid.html.haml +++ b/app/views/projects/merge_requests/invalid.html.haml @@ -1,3 +1,4 @@ +- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" .merge-request = render "projects/merge_requests/show/mr_title" = render "projects/merge_requests/show/mr_box" diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml index 4756903d0e0..b038a640f67 100644 --- a/app/views/projects/merge_requests/new.html.haml +++ b/app/views/projects/merge_requests/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Merge Request" - if @merge_request.can_be_created = render 'new_submit' - else diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index a6587403871..3b7f283daf0 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1,30 +1 @@ -- if @commits.present? - .panel.panel-default - .panel-heading - %i.fa.fa-list - Commits (#{@commits.count}) - .commits.mr-commits - - if @commits.count > 8 - %ul.first-commits.well-list - - @commits.first(8).each do |commit| - = render "projects/commits/commit", commit: commit, project: @merge_request.source_project - %li.bottom - 8 of #{@commits.count} commits displayed. - %strong - %a.show-all-commits Click here to show all - - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - %ul.all-commits.hide.well-list - - @commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE).each do |commit| - = render "projects/commits/inline_commit", commit: commit, project: @merge_request.source_project - %li - other #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} commits hidden to prevent performance issues. - - else - %ul.all-commits.hide.well-list - - @commits.each do |commit| - = render "projects/commits/inline_commit", commit: commit, project: @merge_request.source_project - - - else - %ul.well-list - - @commits.each do |commit| - = render "projects/commits/commit", commit: commit, project: @merge_request.source_project - += render "projects/commits/commits", project: @merge_request.source_project diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index 089302e3588..1d0e2e350b0 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -1,24 +1,48 @@ -= form_for [@project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update'} do |f| - .row - .col-sm-6 - %strong.append-right-10 += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update js-merge-request-update'} do |f| + %div.prepend-top-20 + .issuable-context-title + %label Assignee: - - - if can?(current_user, :modify_merge_request, @merge_request) - = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id) - - elsif merge_request.assignee - = link_to_member(@project, @merge_request.assignee) + - if @merge_request.assignee + %strong= link_to_member(@project, @merge_request.assignee, size: 24) - else - None + none + .issuable-context-selectbox + - if can?(current_user, :modify_merge_request, @merge_request) + = users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id, project: @target_project, null_user: true) - .col-sm-6.text-right - %strong.append-right-10 + %div.prepend-top-20.clearfix + .issuable-context-title + %label Milestone: + - if @merge_request.milestone + %span.back-to-milestone + = link_to namespace_project_milestone_path(@project.namespace, @project, @merge_request.milestone) do + %strong + = icon('clock-o') + = @merge_request.milestone.title + - else + none + .issuable-context-selectbox - if can?(current_user, :modify_merge_request, @merge_request) - = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'}) + = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: 'Select milestone' }, {class: 'select2 select2-compact js-select2 js-milestone'}) = hidden_field_tag :merge_request_context = f.submit class: 'btn' - - elsif merge_request.milestone - = link_to merge_request.milestone.title, project_milestone_path - - else - None + + - if current_user + %div.prepend-top-20.clearfix + .issuable-context-title + %label + Subscription: + %button.btn.btn-block.subscribe-button{:type => 'button'} + = icon('eye') + %span= @merge_request.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe' + - subscribtion_status = @merge_request.subscribed?(current_user) ? 'subscribed' : 'unsubscribed' + .subscription-status{data: {status: subscribtion_status}} + .description-block.unsubscribed{class: ( 'hidden' if @merge_request.subscribed?(current_user) )} + You're not receiving notifications from this thread. + .description-block.subscribed{class: ( 'hidden' unless @merge_request.subscribed?(current_user) )} + You're receiving notifications because you're subscribed to this thread. + +:coffeescript + new Subscription("#{toggle_subscription_namespace_project_merge_request_path(@merge_request.project.namespace, @project, @merge_request)}") diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index d361c5f579a..786b5f39063 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -3,10 +3,10 @@ - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else - .bs-callout.bs-callout-warning + .alert.alert-warning %h4 Changes view for this comparison is extremely large. %p You can - = link_to "download it", project_merge_request_path(@merge_request.target_project, @merge_request, format: :diff), class: "vlink" + = link_to "download it", merge_request_path(@merge_request, format: :diff), class: "vlink" instead. diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index 63db4b30968..22f601ac99e 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -1,4 +1,4 @@ -%div#modal_merge_info.modal.hide +%div#modal_merge_info.modal .modal-dialog .modal-content .modal-header @@ -40,7 +40,6 @@ git merge --no-ff #{@merge_request.source_branch} git push origin #{@merge_request.target_branch} - :javascript $(function(){ var modal = $('#modal_merge_info').modal({modal: true, show:false}); diff --git a/app/views/projects/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml deleted file mode 100644 index 4939ae03994..00000000000 --- a/app/views/projects/merge_requests/show/_mr_accept.html.haml +++ /dev/null @@ -1,70 +0,0 @@ -- unless @allowed_to_merge - - if @project.archived? - %p - %strong Archived projects cannot be committed to! - - else - .automerge_widget.cannot_be_merged.hide - %strong This can't be merged automatically, even if it could be merged you don't have the permission to do so. - .automerge_widget.can_be_merged.hide - %strong This can be merged automatically but you don't have the permission to do so. - - -- if @show_merge_controls - .automerge_widget.can_be_merged.hide - .clearfix - = form_for [:automerge, @project, @merge_request], remote: true, method: :post do |f| - %h4 - You can accept this request automatically. - .accept-merge-holder.clearfix - .accept-group - .pull-left - = f.submit "Accept Merge Request", class: "btn btn-create accept_merge_request" - - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork? - .remove_branch_holder.pull-left - = label_tag :should_remove_source_branch, class: "checkbox" do - = check_box_tag :should_remove_source_branch - Remove source-branch - .js-toggle-container - %label - %i.fa.fa-edit - = link_to "modify merge commit message", "#", class: "modify-merge-commit-link js-toggle-button", title: "Modify merge commit message" - .js-toggle-content.hide - = render 'shared/commit_message_container', params: params, - text: @merge_request.merge_commit_message, - rows: 14, hint: true - - %hr - .light - If you still want to merge this request manually - use - %strong - = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - - - .automerge_widget.no_satellite.hide - %p - %span - %strong This repository does not have satellite. Ask an administrator to fix this issue - - .automerge_widget.cannot_be_merged.hide - %h4 - This request can't be merged with GitLab. - %p - You should do it manually with - %strong - = link_to "command line", "#modal_merge_info", class: "how_to_merge_link", title: "How To Merge", "data-toggle" => "modal" - - .automerge_widget.unchecked - %p - %strong - %i.fa.fa-spinner.fa-spin - Checking for ability to automatically merge… - - .automerge_widget.already_cannot_be_merged.hide - %p - %strong This merge request can not be merged. Try to reload the page. - - .merge-in-progress.hide - %p - %i.fa.fa-spinner.fa-spin - - Merge is in progress. Please wait. Page will be automatically reloaded. 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 7e5a4eda508..b3470ba37d6 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,25 +1,11 @@ -.issue-box{ class: issue_box_class(@merge_request) } - .state.clearfix - .state-label - - if @merge_request.merged? - Merged - - elsif @merge_request.closed? - Closed - - else - Open - - .creator - Created by #{link_to_member(@project, @merge_request.author)} #{time_ago_with_tooltip(@merge_request.created_at)} - - %h4.title - = gfm escape_once(@merge_request.title) +%h2.issue-title + = gfm escape_once(@merge_request.title) +%div - if @merge_request.description.present? - .description + .description{class: can?(current_user, :modify_merge_request, @merge_request) ? 'js-task-list-container' : ''} .wiki = preserve do - = markdown(@merge_request.description, parse_tasks: true) - - .context - %cite.cgray - = render partial: 'projects/merge_requests/show/context', locals: { merge_request: @merge_request } + = markdown(@merge_request.description) + %textarea.hidden.js-task-list-field + = @merge_request.description diff --git a/app/views/projects/merge_requests/show/_mr_ci.html.haml b/app/views/projects/merge_requests/show/_mr_ci.html.haml deleted file mode 100644 index 941b15d3b32..00000000000 --- a/app/views/projects/merge_requests/show/_mr_ci.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- if @commits.any? - .ci_widget.ci-success{style: "display:none"} - %i.fa.fa-check - %span CI build passed - for #{@merge_request.last_commit_short_sha}. - = link_to "Build page", ci_build_details_path(@merge_request) - - - .ci_widget.ci-failed{style: "display:none"} - %i.fa.fa-times - %span CI build failed - for #{@merge_request.last_commit_short_sha}. - = link_to "Build page", ci_build_details_path(@merge_request) - - - [:running, :pending].each do |status| - .ci_widget{class: "ci-#{status}", style: "display:none"} - %i.fa.fa-clock-o - %span CI build #{status} - for #{@merge_request.last_commit_short_sha}. - = link_to "Build page", ci_build_details_path(@merge_request) - - .ci_widget - %i.fa.fa-spinner - Checking for CI status for #{@merge_request.last_commit_short_sha} - - .ci_widget.ci-error{style: "display:none"} - %i.fa.fa-times - %span Cannot connect to the CI server. Please check your settings and try again. 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 6fe765248e4..0690fdb769f 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,45 +1,22 @@ -%h3.page-title +%h4.page-title + .issue-box{ class: issue_box_class(@merge_request) } + - if @merge_request.merged? + Accepted + - elsif @merge_request.closed? + Rejected + - else + Open = "Merge Request ##{@merge_request.iid}" + %small.creator + · + created by #{link_to_member(@project, @merge_request.author)} #{time_ago_with_tooltip(@merge_request.created_at)} - %span.pull-right.issue-btn-group + .issue-btn-group.pull-right - if can?(current_user, :modify_merge_request, @merge_request) - if @merge_request.open? - .btn-group.pull-left - %a.btn.btn-grouped.dropdown-toggle{ data: {toggle: :dropdown} } - %i.fa.fa-download - Download as - %span.caret - %ul.dropdown-menu - %li= link_to "Email Patches", project_merge_request_path(@project, @merge_request, format: :patch) - %li= link_to "Plain Diff", project_merge_request_path(@project, @merge_request, format: :diff) - - = link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-grouped btn-close", title: "Close merge request" - - = link_to edit_project_merge_request_path(@project, @merge_request), class: "btn btn-grouped", id:"edit_merge_request" do + = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-grouped btn-close", title: "Close merge request" + = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn btn-grouped issuable-edit", id: "edit_merge_request" do %i.fa.fa-pencil-square-o Edit - if @merge_request.closed? - = link_to 'Reopen', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link", title: "Close merge request" - -.votes-holder.hidden-sm.hidden-xs - #votes= render 'votes/votes_block', votable: @merge_request - -.back-link - = link_to project_merge_requests_path(@project) do - ← To merge requests - - %span.prepend-left-20 - %span From - - if @merge_request.for_fork? - %strong.label-branch< - - if @merge_request.source_project - = link_to @merge_request.source_project_namespace, project_path(@merge_request.source_project) - - else - \ #{@merge_request.source_project_namespace} - \:#{@merge_request.source_branch} - %span into - %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch} - - else - %strong.label-branch #{@merge_request.source_branch} - %span into - %strong.label-branch #{@merge_request.target_branch} + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link", title: "Close merge request" diff --git a/app/views/projects/merge_requests/show/_participants.html.haml b/app/views/projects/merge_requests/show/_participants.html.haml index b709c89cec2..9c93fa55fe6 100644 --- a/app/views/projects/merge_requests/show/_participants.html.haml +++ b/app/views/projects/merge_requests/show/_participants.html.haml @@ -1,9 +1,4 @@ .participants - %cite.cgray #{@merge_request.participants.count} participants - - @merge_request.participants.each do |participant| + %span #{@merge_request.participants(current_user).count} participants + - @merge_request.participants(current_user).each do |participant| = link_to_member(@project, participant, name: false, size: 24) - - .merge-request-show-labels.pull-right - - @merge_request.labels.each do |label| - = link_to project_merge_requests_path(@project, label_name: label.name) do - = render_colored_label(label) diff --git a/app/views/projects/merge_requests/show/_remove_source_branch.html.haml b/app/views/projects/merge_requests/show/_remove_source_branch.html.haml deleted file mode 100644 index 4fe5935bcf3..00000000000 --- a/app/views/projects/merge_requests/show/_remove_source_branch.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- if @source_branch.blank? - Source branch has been removed - -- elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && @merge_request.merged? - .remove_source_branch_widget - %p Changes merged into #{@merge_request.target_branch}. You can remove source branch now - = link_to project_branch_path(@merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-small remove_source_branch" do - %i.fa.fa-times - Remove Source Branch - - .remove_source_branch_widget.failed.hide - Failed to remove source branch '#{@merge_request.source_branch}' - - .remove_source_branch_in_progress.hide - %i.fa.fa-refresh.fa-spin - - Removing source branch '#{@merge_request.source_branch}'. Please wait. Page will be automatically reloaded. diff --git a/app/views/projects/merge_requests/show/_state_widget.html.haml b/app/views/projects/merge_requests/show/_state_widget.html.haml deleted file mode 100644 index 87dad6140be..00000000000 --- a/app/views/projects/merge_requests/show/_state_widget.html.haml +++ /dev/null @@ -1,47 +0,0 @@ -.mr-state-widget - - if @merge_request.source_project.ci_service && @commits.any? - .mr-widget-heading - = render "projects/merge_requests/show/mr_ci" - .mr-widget-body - - if @merge_request.open? - - if @merge_request.source_branch_exists? && @merge_request.target_branch_exists? - = render "projects/merge_requests/show/mr_accept" - - else - = render "projects/merge_requests/show/no_accept" - - - if @merge_request.closed? - %h4 - Closed by #{link_to_member(@project, @merge_request.closed_event.author, avatar: false)} - #{time_ago_with_tooltip(@merge_request.closed_event.created_at)} - %p Changes were not merged into target branch - - - if @merge_request.merged? - %h4 - Merged by #{link_to_member(@project, @merge_request.merge_event.author, avatar: false)} - #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} - = render "projects/merge_requests/show/remove_source_branch" - - - if @merge_request.locked? - %h4 - Merge in progress... - %p - GitLab tries to merge it right now. During this time merge request is locked and can not be closed. - - - unless @commits.any? - %h4 Nothing to merge - %p - Nothing to merge from - %span.label-branch #{@merge_request.source_branch} - to - %span.label-branch #{@merge_request.target_branch} - %br - Try to use different branches or push new code. - - - if @closes_issues.present? && @merge_request.open? - .mr-widget-footer - %span - %i.fa.fa-check - Accepting this merge request will close #{@closes_issues.size == 1 ? 'issue' : 'issues'} - = succeed '.' do - != gfm(issues_sentence(@closes_issues)) - diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml index 6452cc6382d..b4df1d20737 100644 --- a/app/views/projects/merge_requests/update.js.haml +++ b/app/views/projects/merge_requests/update.js.haml @@ -1,2 +1,8 @@ - if params[:merge_request_context] - $('.issue-box .context').effect('highlight'); + $('.context').html("#{escape_javascript(render partial: 'projects/merge_requests/show/context', locals: { issue: @issue })}"); + $('.context').effect('highlight'); + + new UsersSelect() + + $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}); + merge_request = new MergeRequest(); diff --git a/app/views/projects/merge_requests/widget/_closed.html.haml b/app/views/projects/merge_requests/widget/_closed.html.haml new file mode 100644 index 00000000000..18164ba771f --- /dev/null +++ b/app/views/projects/merge_requests/widget/_closed.html.haml @@ -0,0 +1,9 @@ +.mr-state-widget + = render 'projects/merge_requests/widget/heading' + .mr-widget-body + %h4 + Rejected + - if @merge_request.closed_event + by #{link_to_member(@project, @merge_request.closed_event.author, avatar: true)} + #{time_ago_with_tooltip(@merge_request.closed_event.created_at)} + %p Changes were not merged into target branch diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml new file mode 100644 index 00000000000..107c61477e3 --- /dev/null +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -0,0 +1,38 @@ +- if @merge_request.has_ci? + .mr-widget-heading + .ci_widget.ci-success{style: "display:none"} + = icon("check") + %span CI build passed + for #{@merge_request.last_commit_short_sha}. + = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + + .ci_widget.ci-failed{style: "display:none"} + = icon("times") + %span CI build failed + for #{@merge_request.last_commit_short_sha}. + = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + + - [:running, :pending].each do |status| + .ci_widget{class: "ci-#{status}", style: "display:none"} + = icon("clock-o") + %span CI build #{status} + for #{@merge_request.last_commit_short_sha}. + = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + + .ci_widget + = icon("spinner spin") + Checking for CI status for #{@merge_request.last_commit_short_sha} + + .ci_widget.ci-canceled{style: "display:none"} + = icon("times") + %span CI build canceled + for #{@merge_request.last_commit_short_sha}. + = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + + .ci_widget.ci-error{style: "display:none"} + = icon("times") + %span Cannot connect to the CI server. Please check your settings and try again. + + :coffeescript + $ -> + merge_request_widget.getCiStatus() diff --git a/app/views/projects/merge_requests/widget/_locked.html.haml b/app/views/projects/merge_requests/widget/_locked.html.haml new file mode 100644 index 00000000000..13ec278847b --- /dev/null +++ b/app/views/projects/merge_requests/widget/_locked.html.haml @@ -0,0 +1,8 @@ +.mr-state-widget + = render 'projects/merge_requests/widget/heading' + .mr-widget-body + %h4 + Merge in progress... + %p + Merging is in progress. While merging this request is locked and cannot be closed. + diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml new file mode 100644 index 00000000000..17c3fdacda8 --- /dev/null +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -0,0 +1,41 @@ +.mr-state-widget + = render 'projects/merge_requests/widget/heading' + .mr-widget-body + %h4 + Accepted + - if @merge_request.merge_event + by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)} + #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} + %div + - if @source_branch.blank? + Source branch has been removed + + - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && @merge_request.merged? + .remove_source_branch_widget + %p Changes merged into #{@merge_request.target_branch}. You can remove source branch now + = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do + %i.fa.fa-times + Remove Source Branch + + .remove_source_branch_widget.failed.hide + Failed to remove source branch '#{@merge_request.source_branch}' + + .remove_source_branch_in_progress.hide + %i.fa.fa-spinner.fa-spin + + Removing source branch '#{@merge_request.source_branch}'. Please wait. Page will be automatically reloaded. + + :coffeescript + $('.remove_source_branch').on 'click', -> + $('.remove_source_branch_widget').hide() + $('.remove_source_branch_in_progress').show() + + $(".remove_source_branch").on "ajax:success", (e, data, status, xhr) -> + location.reload() + + $(".remove_source_branch").on "ajax:error", (e, data, status, xhr) -> + $('.remove_source_branch_widget').hide() + $('.remove_source_branch_in_progress').hide() + $('.remove_source_branch_widget.failed').show() + + diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml new file mode 100644 index 00000000000..bb794912f8f --- /dev/null +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -0,0 +1,29 @@ +.mr-state-widget + = render 'projects/merge_requests/widget/heading' + .mr-widget-body + - if @project.archived? + = render 'projects/merge_requests/widget/open/archived' + - elsif !@project.satellite.exists? + = render 'projects/merge_requests/widget/open/no_satellite' + - elsif @merge_request.commits.blank? + = render 'projects/merge_requests/widget/open/nothing' + - elsif @merge_request.branch_missing? + = render 'projects/merge_requests/widget/open/missing_branch' + - elsif @merge_request.unchecked? + = render 'projects/merge_requests/widget/open/check' + - elsif @merge_request.cannot_be_merged? + = render 'projects/merge_requests/widget/open/conflicts' + - elsif @merge_request.work_in_progress? + = render 'projects/merge_requests/widget/open/wip' + - elsif !@merge_request.can_be_merged_by?(current_user) + = render 'projects/merge_requests/widget/open/not_allowed' + - elsif @merge_request.can_be_merged? + = render 'projects/merge_requests/widget/open/accept' + + - if @closes_issues.present? + .mr-widget-footer + %span + %i.fa.fa-check + Accepting this merge request will close #{@closes_issues.size == 1 ? 'issue' : 'issues'} + = succeed '.' do + != gfm(issues_sentence(@closes_issues)) diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml new file mode 100644 index 00000000000..263cab7a9e8 --- /dev/null +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -0,0 +1,20 @@ +- if @merge_request.open? + = render 'projects/merge_requests/widget/open' +- elsif @merge_request.merged? + = render 'projects/merge_requests/widget/merged' +- elsif @merge_request.closed? + = render 'projects/merge_requests/widget/closed' +- elsif @merge_request.locked? + = render 'projects/merge_requests/widget/locked' + +:javascript + var merge_request_widget; + + merge_request_widget = new MergeRequestWidget({ + url_to_automerge_check: "#{automerge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + check_enable: #{@merge_request.unchecked? ? "true" : "false"}, + url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + ci_enable: #{@project.ci_service ? "true" : "false"}, + current_status: "#{@merge_request.automerge_status}", + }); + diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml new file mode 100644 index 00000000000..41aa66dd76b --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -0,0 +1,34 @@ += form_for [:automerge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form' } do |f| + = hidden_field_tag :authenticity_token, form_authenticity_token + .accept-merge-holder.clearfix.js-toggle-container + .accept-action + = f.button class: "btn btn-create accept_merge_request" do + Accept Merge Request + - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork? + .accept-control.checkbox + = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do + = check_box_tag :should_remove_source_branch + Remove source-branch + .accept-control + = link_to "#", class: "modify-merge-commit-link js-toggle-button", title: "Modify merge commit message" do + %i.fa.fa-edit + Modify commit message + .js-toggle-content.hide.prepend-top-20 + = render 'shared/commit_message_container', params: params, + text: @merge_request.merge_commit_message, + rows: 14, hint: true + + %br + .light + If you want to merge this request manually, you can use the + %strong + = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" + + :coffeescript + disableButtonIfEmptyField '#commit_message', '.accept_merge_request' + + $('.accept-mr-form').on 'ajax:before', -> + btn = $('.accept_merge_request') + btn.disable() + btn.html("<i class='fa fa-spinner fa-spin'></i> Merge in progress") + diff --git a/app/views/projects/merge_requests/widget/open/_archived.html.haml b/app/views/projects/merge_requests/widget/open/_archived.html.haml new file mode 100644 index 00000000000..eaf113ee568 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_archived.html.haml @@ -0,0 +1,2 @@ +%p + %strong Archived projects do not provide commit access. diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml new file mode 100644 index 00000000000..e775447cb75 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_check.html.haml @@ -0,0 +1,7 @@ +%strong + %i.fa.fa-spinner.fa-spin + Checking automatic merge… + +:coffeescript + $ -> + merge_request_widget.getMergeStatus() diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml new file mode 100644 index 00000000000..d1db5fec43a --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml @@ -0,0 +1,9 @@ +- if @merge_request.can_be_merged_by?(current_user) + %h4 + This merge request contains merge conflicts that must be resolved. + You can try it manually on the + %strong + = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" +- else + %strong This merge request contains merge conflicts that must be resolved. + Only those with write access to this repository can merge merge requests. diff --git a/app/views/projects/merge_requests/show/_no_accept.html.haml b/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml index 423fcd48e25..423fcd48e25 100644 --- a/app/views/projects/merge_requests/show/_no_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml diff --git a/app/views/projects/merge_requests/widget/open/_no_satellite.html.haml b/app/views/projects/merge_requests/widget/open/_no_satellite.html.haml new file mode 100644 index 00000000000..3718cfd8333 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_no_satellite.html.haml @@ -0,0 +1,3 @@ +%p + %span + %strong This repository does not have a satellite. Please ask an administrator to fix this issue! 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 new file mode 100644 index 00000000000..82f6ffd8fcb --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml @@ -0,0 +1,2 @@ +%strong This request can be merged automatically. +Only those with write access to this repository can merge merge requests. diff --git a/app/views/projects/merge_requests/widget/open/_nothing.html.haml b/app/views/projects/merge_requests/widget/open/_nothing.html.haml new file mode 100644 index 00000000000..4d526576bc2 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_nothing.html.haml @@ -0,0 +1,8 @@ +%h4 Nothing to merge +%p + Nothing to merge from + %span.label-branch #{@merge_request.source_branch} + to + %span.label-branch #{@merge_request.target_branch} + %br + Try to use different branches or push new code. diff --git a/app/views/projects/merge_requests/widget/open/_reload.html.haml b/app/views/projects/merge_requests/widget/open/_reload.html.haml new file mode 100644 index 00000000000..5787f6efea4 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_reload.html.haml @@ -0,0 +1 @@ +This merge request cannot be merged. Try to reload the page. diff --git a/app/views/projects/merge_requests/widget/open/_wip.html.haml b/app/views/projects/merge_requests/widget/open/_wip.html.haml new file mode 100644 index 00000000000..4ce3ab31278 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_wip.html.haml @@ -0,0 +1,13 @@ +- if @merge_request.can_be_merged_by?(current_user) + %h4 + This merge request cannot be accepted because it is marked as Work In Progress. + + %p + %button.btn.disabled{:type => 'button'} + %i.fa.fa-warning + Accept Merge Request + + When the merge request is ready, remove the "WIP" prefix from the title to allow it to be accepted. +- else + %strong This merge request is marked as Work In Progress. + Only those with write access to this repository can merge merge requests. diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 5fb01a11cc5..5650607f31f 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,11 +1,11 @@ %h3.page-title= @milestone.new_record? ? "New Milestone" : "Edit Milestone ##{@milestone.iid}" .back-link - = link_to project_milestones_path(@project) do + = link_to namespace_project_milestones_path(@project.namespace, @project) do ← To milestones %hr -= form_for [@project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form'} do |f| += form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form'} do |f| -if @milestone.errors.any? .alert.alert-danger %ul @@ -18,13 +18,14 @@ .col-sm-10 = f.text_field :title, maxlength: 255, class: "form-control" %p.hint Required - .form-group + .form-group.milestone-description = f.label :description, "Description", class: "control-label" .col-sm-10 - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' - .hint - .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. - .pull-left Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do + = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + .hint + .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. + .pull-left Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. .clearfix .error-alert .col-md-6 @@ -37,10 +38,10 @@ .form-actions - if @milestone.new_record? = f.submit 'Create milestone', class: "btn-create btn" - = link_to "Cancel", project_milestones_path(@project), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_milestones_path(@project.namespace, @project), class: "btn btn-cancel" -else = f.submit 'Save changes', class: "btn-save btn" - = link_to "Cancel", project_milestone_path(@project, @milestone), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-cancel" :javascript @@ -49,5 +50,3 @@ dateFormat: "yy-mm-dd", onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); - - window.project_image_path_upload = "#{upload_image_project_path @project}"; diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml index b5ec0fc9882..88fccfe4981 100644 --- a/app/views/projects/milestones/_issue.html.haml +++ b/app/views/projects/milestones/_issue.html.haml @@ -1,9 +1,9 @@ -%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => project_issue_path(@project, issue) } - %span.str-truncated - = link_to [@project, issue] do - %span.cgray ##{issue.iid} - = link_to_gfm issue.title, [@project, issue], title: issue.title +%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) } .pull-right.assignee-icon - if issue.assignee - = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16" + = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16", alt: '' + %span + = link_to [@project.namespace.becomes(Namespace), @project, issue] do + %span.cgray ##{issue.iid} + = link_to_gfm issue.title, [@project.namespace.becomes(Namespace), @project, issue], title: issue.title diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml index d54cb3f8e74..0d7a118569a 100644 --- a/app/views/projects/milestones/_merge_request.html.haml +++ b/app/views/projects/milestones/_merge_request.html.haml @@ -1,5 +1,8 @@ -%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => project_merge_request_path(@project, merge_request) } +%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => merge_request_path(merge_request) } %span.str-truncated - = link_to [@project, merge_request] do + = link_to [@project.namespace.becomes(Namespace), @project, merge_request] do %span.cgray ##{merge_request.iid} - = link_to_gfm merge_request.title, [@project, merge_request], title: merge_request.title + = link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title + .pull-right.assignee-icon + - if merge_request.assignee + = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16", alt: '' diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 1002b9513ff..14a0580f966 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -1,27 +1,24 @@ %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) } .pull-right - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-small edit-milestone-link btn-grouped" do + = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-sm edit-milestone-link btn-grouped" do %i.fa.fa-pencil-square-o Edit - = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-small btn-close" + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close" %h4 - = link_to_gfm truncate(milestone.title, length: 100), project_milestone_path(milestone.project, milestone) + = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - if milestone.expired? and not milestone.closed? %span.cred (Expired) %small = milestone.expires_at - - if milestone.is_empty? - %span.muted Empty - - else - %div - %div - = link_to project_issues_path(milestone.project, milestone_id: milestone.id) do - = pluralize milestone.issues.count, 'Issue' - - = link_to project_merge_requests_path(milestone.project, milestone_id: milestone.id) do - = pluralize milestone.merge_requests.count, 'Merge Request' - - %span.light #{milestone.percent_complete}% complete - .progress.progress-info - .progress-bar{style: "width: #{milestone.percent_complete}%;"} + .row + .col-sm-6 + = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do + = pluralize milestone.issues.count, 'Issue' + + = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do + = pluralize milestone.merge_requests.count, 'Merge Request' + + %span.light #{milestone.percent_complete}% complete + .col-sm-6 + = milestone_progress_bar(milestone) diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index b1bc3ba0eba..c09815a212a 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -1 +1,2 @@ +- page_title "Edit", @milestone.title, "Milestones" = render "form" diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 0db0b114d63..995eecd7830 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,33 +1,18 @@ -= render "projects/issues_nav" -.milestones_content - %h3.page-title - Milestones - - if can? current_user, :admin_milestone, @project - = link_to new_project_milestone_path(@project), class: "pull-right btn btn-new", title: "New Milestone" do - %i.fa.fa-plus - New Milestone +- page_title "Milestones" +.pull-right + - if can? current_user, :admin_milestone, @project + = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do + %i.fa.fa-plus + New Milestone += render 'shared/milestones_filter' - .row - .fixed.sidebar-expand-button.hidden-lg.hidden-md.hidden-xs - %i.fa.fa-list.fa-2x - .col-md-3.responsive-side - %ul.nav.nav-pills.nav-stacked - %li{class: ("active" if (params[:f] == "active" || !params[:f]))} - = link_to project_milestones_path(@project, f: "active") do - Active - %li{class: ("active" if params[:f] == "closed")} - = link_to project_milestones_path(@project, f: "closed") do - Closed - %li{class: ("active" if params[:f] == "all")} - = link_to project_milestones_path(@project, f: "all") do - All - .col-md-9 - .panel.panel-default - %ul.well-list - = render @milestones +.milestones + .panel.panel-default + %ul.well-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 b1bc3ba0eba..47149dfea41 100644 --- a/app/views/projects/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml @@ -1 +1,2 @@ +- page_title "New Milestone" = render "form" diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index f08ccc1d570..417eaa1b09d 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,57 +1,50 @@ -= render "projects/issues_nav" -%h3.page-title +- page_title @milestone.title, "Milestones" +%h4.page-title + .issue-box{ class: issue_box_class(@milestone) } + - if @milestone.closed? + Closed + - elsif @milestone.expired? + Expired + - else + Open Milestone ##{@milestone.iid} + %small.creator + = @milestone.expires_at .pull-right - if can?(current_user, :admin_milestone, @project) - = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped" do + = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do %i.fa.fa-pencil-square-o Edit - if @milestone.active? - = link_to 'Close Milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped" + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped" - else - = link_to 'Reopen Milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" + = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" +%hr - if @milestone.issues.any? && @milestone.can_be_closed? .alert.alert-success %span All issues for this milestone are closed. You may close milestone now. -.back-link - = link_to project_milestones_path(@project) do - ← To milestones list - - -.issue-box{ class: issue_box_class(@milestone) } - .state.clearfix - .state-label - - if @milestone.closed? - Closed - - elsif @milestone.expired? - Expired - - else - Open - .creator - = @milestone.expires_at - - %h4.title - = gfm escape_once(@milestone.title) - +%h3.issue-title + = gfm escape_once(@milestone.title) +%div - if @milestone.description.present? .description .wiki = preserve do = markdown @milestone.description - .context - %p - Progress: - #{@milestone.closed_items_count} closed - – - #{@milestone.open_items_count} open - - %span.light #{@milestone.percent_complete}% complete - %span.pull-right= @milestone.expires_at - .progress.progress-info - .progress-bar{style: "width: #{@milestone.percent_complete}%;"} +%hr +.context + %p.lead + Progress: + #{@milestone.closed_items_count} closed + – + #{@milestone.open_items_count} open + + %span.light #{@milestone.percent_complete}% complete + %span.pull-right= @milestone.expires_at + = milestone_progress_bar(@milestone) %ul.nav.nav-tabs @@ -69,10 +62,12 @@ %span.badge= @users.count .pull-right - = link_to new_project_issue_path(@project, issue: { milestone_id: @milestone.id }), class: "btn btn-small btn-grouped", title: "New Issue" do - %i.fa.fa-plus - New Issue - = link_to 'Browse Issues', project_issues_path(@milestone.project, milestone_id: @milestone.id), class: "btn btn-small edit-milestone-link btn-grouped" + - if can?(current_user, :write_issue, @project) + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do + %i.fa.fa-plus + New Issue + - if can?(current_user, :read_issue, @project) + = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped" .tab-content .tab-pane.active#tab-issues @@ -91,10 +86,10 @@ .col-md-3 = render('merge_requests', title: 'Waiting for merge (open and assigned)', merge_requests: @merge_requests.opened.assigned, id: 'ongoing') .col-md-3 - = render('merge_requests', title: 'Declined (closed)', merge_requests: @merge_requests.declined, id: 'closed') + = render('merge_requests', title: 'Rejected (closed)', merge_requests: @merge_requests.rejected, id: 'closed') .col-md-3 .panel.panel-primary - .panel-heading Merged + .panel-heading Accepted %ul.well-list - @merge_requests.merged.each do |merge_request| = render 'merge_request', merge_request: merge_request diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 4a21b84fb85..c67a7d256a8 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,7 +1,8 @@ +- page_title "Network", @ref = render "head" .project-network .controls - = form_tag project_network_path(@project, @id), method: :get, class: 'form-inline network-form' do |f| + = 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 btn-search-sha' do %i.fa.fa-search @@ -18,8 +19,8 @@ disableButtonIfEmptyField('#extended_sha1', '.btn-search-sha') network_graph = new Network({ - url: '#{project_network_path(@project, @ref, @options.merge(format: :json))}', - commit_url: '#{project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")}', + url: '#{namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json))}', + commit_url: '#{namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s")}', ref: '#{@ref}', commit_id: '#{@commit.id}' }) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index e77ef84f51c..e56d8615132 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,3 +1,5 @@ +- page_title 'New Project' +- header_title 'New Project' .project-edit-container .project-edit-errors = render 'projects/errors' @@ -5,10 +7,13 @@ = form_for @project, html: { class: 'new_project form-horizontal' } do |f| .form-group.project-name-holder - = f.label :name, class: 'control-label' do - %strong Project name + = f.label :path, class: 'control-label' do + %strong Project path .col-sm-10 - = f.text_field :name, placeholder: "Example Project", class: "form-control", tabindex: 1, autofocus: true + .input-group + = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 1, autofocus: true + .input-group-addon + \.git - if current_user.can_select_namespace? .form-group @@ -18,41 +23,71 @@ = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2} %hr - .js-toggle-container - .form-group - .col-sm-2 - .col-sm-10 - = link_to "#", class: 'js-toggle-button' do - %i.fa.fa-pencil-square-o - %span Customize repository name? - .js-toggle-content.hide - .form-group - = f.label :path, class: 'control-label' do - %span Repository name - .col-sm-10 - .input-group - = f.text_field :path, class: 'form-control' - %span.input-group-addon .git - .js-toggle-container + .project-import.js-toggle-container .form-group - .col-sm-2 + %label.control-label Import project from .col-sm-10 - = link_to "#", class: 'js-toggle-button' do - %i.fa.fa-upload - %span Import existing repository? + - if github_import_enabled? + = link_to status_import_github_path, class: 'btn' do + %i.fa.fa-github + GitHub + - else + = link_to '#', class: 'how_to_import_link light btn' do + %i.fa.fa-github + GitHub + = render 'github_import_modal' + + + - if bitbucket_import_enabled? + = link_to status_import_bitbucket_path, class: 'btn' do + %i.fa.fa-bitbucket + Bitbucket + - else + = link_to '#', class: 'how_to_import_link light btn' do + %i.fa.fa-bitbucket + Bitbucket + = render 'bitbucket_import_modal' + + - unless request.host == 'gitlab.com' + - if gitlab_import_enabled? + = link_to status_import_gitlab_path, class: 'btn' do + %i.fa.fa-heart + GitLab.com + - else + = link_to '#', class: 'how_to_import_link light btn' do + %i.fa.fa-heart + GitLab.com + = render 'gitlab_import_modal' + + = link_to new_import_gitorious_path, class: 'btn' do + %i.icon-gitorious.icon-gitorious-small + Gitorious.org + + = link_to new_import_google_code_path, class: 'btn' do + %i.fa.fa-google + Google Code + + = link_to "#", class: 'btn js-toggle-button' do + %i.fa.fa-git + %span Any repo by URL + .js-toggle-content.hide .form-group.import-url-data = f.label :import_url, class: 'control-label' do - %span Import existing git repo + %span Git repository URL .col-sm-10 - = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' - .bs-callout.bs-callout-info - This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. - %br - The import will time out after 4 minutes. For big repositories, use a clone/push combination. - For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"} - %hr + = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' + .well.prepend-top-20 + %ul + %li + The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. + %li + The import will time out after 4 minutes. For big repositories, use a clone/push combination. + %li + To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"}. + + %hr.prepend-botton-10 .form-group = f.label :description, class: 'control-label' do @@ -60,7 +95,7 @@ %span.light (optional) .col-sm-10 = f.text_area :description, placeholder: "Awesome project", class: "form-control", rows: 3, maxlength: 250, tabindex: 3 - = render "visibility_level", f: f, visibility_level: gitlab_config.default_projects_features.visibility_level, can_change_visibility_level: true + = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project .form-actions = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 @@ -69,7 +104,7 @@ .pull-right .light Need a group for several dependent projects? - = link_to new_group_path, class: "btn btn-tiny" do + = link_to new_group_path, class: "btn btn-xs" do Create a group .save-project-loader.hide @@ -78,3 +113,10 @@ %i.fa.fa-spinner.fa-spin Creating project & repository. %p Please wait a moment, this page will automatically refresh when ready. + +:coffeescript + $('.how_to_import_link').bind 'click', (e) -> + e.preventDefault() + import_modal = $(this).next(".modal").show() + $('.modal-header .close').bind 'click', -> + $(".modal").hide() diff --git a/app/views/projects/new_tree/show.html.haml b/app/views/projects/new_tree/show.html.haml deleted file mode 100644 index f09d3659774..00000000000 --- a/app/views/projects/new_tree/show.html.haml +++ /dev/null @@ -1,43 +0,0 @@ -%h3.page-title New file -%hr -.file-editor - = form_tag(project_new_tree_path(@project, @id), method: :put, class: 'form-horizontal form-new-file') do - .form-group.commit_message-group - = label_tag 'file_name', class: 'control-label' do - File name - .col-sm-10 - .input-group - %span.input-group-addon - = @path[-1] == "/" ? @path : @path + "/" - = text_field_tag 'file_name', params[:file_name], placeholder: "sample.rb", required: true, class: 'form-control' - %span.input-group-addon - on - %span= @ref - - .form-group.commit_message-group - = label_tag :encoding, class: "control-label" do - Encoding - .col-sm-10 - = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' - .file-holder - .file-title - %i.fa.fa-file - .file-content.code - %pre#editor= params[:content] - - = render 'shared/commit_message_container', params: params, - placeholder: 'Add new file' - = hidden_field_tag 'content', '', id: 'file-content' - = render 'projects/commit_button', ref: @ref, - cancel_path: project_tree_path(@project, @id) - -:javascript - ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace-src-noconflict") - var editor = ace.edit("editor"); - - disableButtonIfAnyEmptyField($('.form-new-file'), '.form-control', '.btn-create') - - $(".js-commit-button").click(function(){ - $("#file-content").val(editor.getValue()); - $(".file-editor form").submit(); - }); diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml index dd576243510..720957e8336 100644 --- a/app/views/projects/no_repo.html.haml +++ b/app/views/projects/no_repo.html.haml @@ -9,14 +9,14 @@ %hr .no-repo-actions - = link_to project_repository_path(@project), method: :post, class: 'btn btn-primary' do + = link_to namespace_project_repository_path(@project.namespace, @project), method: :post, class: 'btn btn-primary' do Create empty bare repository %strong.prepend-left-10.append-right-10 or - = link_to new_project_import_path(@project), class: 'btn' do + = link_to new_namespace_project_import_path(@project.namespace, @project), class: 'btn' do Import repository - if can? current_user, :remove_project, @project .prepend-top-20 - = link_to 'Remove project', @project, data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" + = link_to 'Remove project', project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" 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 789f3e19fd2..c6726cbafa3 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,34 +1,34 @@ -- note1 = notes1.present? ? notes1.first : nil -- note2 = notes2.present? ? notes2.first : nil +- note1 = notes_left.present? ? notes_left.first : nil +- note2 = notes_right.present? ? notes_right.first : nil %tr.notes_holder - if note1 - %td.notes_line + %td.notes_line.old %span.btn.disabled %i.fa.fa-comment - = notes1.count - %td.notes_content.parallel + = notes_left.count + %td.notes_content.parallel.old %ul.notes{ rel: note1.discussion_id } - = render notes1 + = render notes_left .discussion-reply-holder - = link_to_reply_diff(note1) + = link_to_reply_diff(note1, 'old') - else - %td= "" - %td= "" + %td.notes_line.old= "" + %td.notes_content.parallel.old= "" - if note2 - %td.notes_line + %td.notes_line.new %span.btn.disabled %i.fa.fa-comment - = notes2.count - %td.notes_content.parallel + = notes_right.count + %td.notes_content.parallel.new %ul.notes{ rel: note2.discussion_id } - = render notes2 + = render notes_right .discussion-reply-holder - = link_to_reply_diff(note2) + = link_to_reply_diff(note2, 'new') - else - %td= "" - %td= "" + %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 f4c6fad2fed..b8068835b3a 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -2,12 +2,12 @@ .timeline-entry .timeline-entry-inner .timeline-icon - = image_tag avatar_icon(note.author_email), class: "avatar s40" + = link_to user_path(note.author) do + = image_tag avatar_icon(note.author_email), class: "avatar s40" .timeline-content - if note.for_merge_request? - - if note.outdated? - = render "projects/notes/discussions/outdated", discussion_notes: discussion_notes - - else - = render "projects/notes/discussions/active", discussion_notes: discussion_notes + - (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 diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml new file mode 100644 index 00000000000..a663950f031 --- /dev/null +++ b/app/views/projects/notes/_edit_form.html.haml @@ -0,0 +1,14 @@ +.note-edit-form + = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f| + = note_target_fields(note) + = render layout: 'projects/md_preview', locals: { preview_class: 'note-text' } do + = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' + + .comment-hints.clearfix + .pull-left Comments are parsed with #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'),{ target: '_blank', tabindex: -1 }} + .pull-right Attach files by dragging & dropping or #{link_to 'selecting them', '#', class: 'markdown-selector', tabindex: -1 }. + + .note-form-actions + .buttons + = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button' + = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel' diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 5bc0e60bbe8..3fb044d736e 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,41 +1,23 @@ -= form_for [@project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f| - = note_target_fields += form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f| + = hidden_field_tag :view, params[:view] + = hidden_field_tag :line_type + = note_target_fields(@note) = f.hidden_field :commit_id = f.hidden_field :line_code = f.hidden_field :noteable_id = f.hidden_field :noteable_type - %ul.nav.nav-tabs - %li.active - = link_to '#note-write-holder', class: 'js-note-write-button' do - Write - %li - = link_to '#note-preview-holder', class: 'js-note-preview-button', data: { url: preview_project_notes_path(@project) } do - Preview - %div - .note-write-holder - = render 'projects/zen', f: f, attr: :note, - classes: 'note_text js-note-text' - .light.clearfix - .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }} - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. + = render layout: 'projects/md_preview', locals: { preview_class: "note-text", referenced_users: true } do + = render 'projects/zen', f: f, attr: :note, + classes: 'note_text js-note-text' - .note-preview-holder.hide - .js-note-preview + .comment-hints.clearfix + .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }} + .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. + .error-alert .note-form-actions - .buttons + .buttons.clearfix = f.submit 'Add Comment', class: "btn comment-btn btn-grouped js-comment-button" = yield(:note_actions) %a.btn.grouped.js-close-discussion-note-form Cancel - - .note-form-option.hidden-xs - %a.choose-btn.btn.js-choose-note-attachment-button - %i.fa.fa-paperclip - %span Choose File ... - - %span.file_name.js-attachment-filename File name... - = f.file_field :attachment, class: "js-note-attachment-input hidden" - -:javascript - window.project_image_path_upload = "#{upload_image_project_path @project}"; diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index b2abdf0035d..0a77f200f56 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -1,71 +1,76 @@ -%li.timeline-entry{ id: dom_id(note), class: dom_class(note), data: { discussion: note.discussion_id } } +%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: { discussion: note.discussion_id } } .timeline-entry-inner .timeline-icon - = image_tag avatar_icon(note.author_email), class: "avatar s40" + - if note.system + %span= icon('circle') + - else + = link_to user_path(note.author) do + = image_tag avatar_icon(note.author_email), class: 'avatar s40', alt: '' .timeline-content .note-header - .note-actions - = link_to "##{dom_id(note)}", name: dom_id(note) do - %i.fa.fa-link - Link here - - - if can?(current_user, :admin_note, note) && note.editable? - = link_to "#", title: "Edit comment", class: "js-note-edit" do - %i.fa.fa-pencil-square-o - Edit - - = link_to project_note_path(@project, note), title: "Remove comment", method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: "danger js-note-delete" do - %i.fa.fa-trash-o.cred - Remove - = link_to_member(@project, note.author, avatar: false) + - if note_editable?(note) + .note-actions + = link_to '#', title: 'Edit comment', class: 'js-note-edit' do + = icon('pencil-square-o') + + = 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: 'js-note-delete danger' do + = icon('trash-o') + + - unless note.system + - member = note.project.team.find_member(note.author.id) + - if member + %span.note-role.label + = member.human_access + + - if note.system + = link_to user_path(note.author) do + = image_tag avatar_icon(note.author_email), class: 'avatar s16', alt: '' + + = link_to_member(note.project, note.author, avatar: false) + %span.author-username = '@' + note.author.username + %span.note-last-update - = note_timestamp(note) + = link_to "##{dom_id(note)}", name: dom_id(note), title: "Link here" do + = note_timestamp(note) - - if note.upvote? - %span.vote.upvote.label.label-success - %i.fa.fa-thumbs-up - \+1 - - if note.downvote? - %span.vote.downvote.label.label-danger - %i.fa.fa-thumbs-down - \-1 + - if note.superceded?(@notes) + - if note.upvote? + %span.vote.upvote.label.label-gray.strikethrough + = icon('thumbs-up') + \+1 + - if note.downvote? + %span.vote.downvote.label.label-gray.strikethrough + = icon('thumbs-down') + \-1 + - else + - if note.upvote? + %span.vote.upvote.label.label-success + = icon('thumbs-up') + \+1 + - if note.downvote? + %span.vote.downvote.label.label-danger + = icon('thumbs-down') + \-1 - .note-body + .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text = preserve do = markdown(note.note, {no_header_anchors: true}) - - .note-edit-form - = form_for note, url: project_note_path(@project, note), method: :put, remote: true, authenticity_token: true do |f| - = f.text_area :note, class: 'note_text js-note-text js-gfm-input turn-on' - - .form-actions.clearfix - = f.submit 'Save changes', class: "btn btn-primary btn-save js-comment-button" - - .note-form-option - %a.choose-btn.btn.js-choose-note-attachment-button - %i.fa.fa-paperclip - %span Choose File ... - - %span.file_name.js-attachment-filename File name... - = f.file_field :attachment, class: "js-note-attachment-input hidden" - - = link_to 'Cancel', "#", class: "btn btn-cancel note-edit-cancel" - + = render 'projects/notes/edit_form', note: note - if note.attachment.url .note-attachment - if note.attachment.image? - = link_to note.attachment.secure_url, target: '_blank' do - = image_tag note.attachment.secure_url, class: 'note-image-attach' + = link_to note.attachment.url, target: '_blank' do + = image_tag note.attachment.url, class: 'note-image-attach' .attachment - = link_to note.attachment.secure_url, target: "_blank" do - %i.fa.fa-paperclip + = link_to note.attachment.url, target: '_blank' do + = icon('paperclip') = note.attachment_identifier - = link_to delete_attachment_project_note_path(@project, note), - title: "Delete this attachment", method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: "danger js-note-attachment-delete" do - %i.fa.fa-trash-o.cred + = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note), + title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do + = icon('trash-o', class: 'cred') .clear diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 04ee17a40a0..a202e74a892 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -4,7 +4,7 @@ .js-main-target-form - if can? current_user, :write_note, @project - = render "projects/notes/form" + = render "projects/notes/form", view: params[:view] :javascript - new Notes("#{project_notes_path(target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}) + new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{params[:view]}") diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml index 52c06ec172d..4f15a99d061 100644 --- a/app/views/projects/notes/discussions/_active.html.haml +++ b/app/views/projects/notes/discussions/_active.html.haml @@ -8,13 +8,15 @@ %div = link_to_member(@project, note.author, avatar: false) started a discussion - = link_to diffs_project_merge_request_path(note.project, note.noteable, anchor: note.line_code) do + = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do %strong on the diff .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) + %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, 'bottom', 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + .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 index 94f16a5f02e..6903fad4a0a 100644 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ b/app/views/projects/notes/discussions/_commit.html.haml @@ -8,13 +8,13 @@ %div = link_to_member(@project, note.author, avatar: false) started a discussion on commit - = link_to(note.noteable.short_id, project_commit_path(note.project, note.noteable), class: 'monospace') + = link_to(note.noteable.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, 'bottom', 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content - if note.for_diff_line? = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index b4d1cce7980..711aa39101b 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -2,13 +2,13 @@ - if diff .diff-file .diff-header - - if diff.deleted_file - %span= diff.old_path - - else - %span= 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}" - %br/ + %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 %table - note.truncated_diff_lines.each do |line| @@ -19,8 +19,10 @@ %td.new_line= "..." %td.line_content.matched= line.text - else - %td.old_line= raw(line.type == "new" ? " " : line.old_pos) - %td.new_line= raw(line.type == "old" ? " " : line.new_pos) + %td.old_line{class: line.type == "new" ? "new" : "old"} + = raw(line.type == "new" ? " " : line.old_pos) + %td.new_line{class: line.type == "new" ? "new" : "old"} + = raw(line.type == "old" ? " " : line.new_pos) %td.line_content{class: "noteable_line #{line.type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text) - if line_code == note.line_code diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml index 52a1d342f55..218b0da3977 100644 --- a/app/views/projects/notes/discussions/_outdated.html.haml +++ b/app/views/projects/notes/discussions/_outdated.html.haml @@ -14,6 +14,6 @@ last updated by = link_to_member(@project, last_note.author, avatar: false) %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, 'bottom', 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content.hide = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml new file mode 100644 index 00000000000..43e92437cf5 --- /dev/null +++ b/app/views/projects/project_members/_group_members.html.haml @@ -0,0 +1,16 @@ +.panel.panel-default + .panel-heading + %strong #{@group.name} + group members + %small + (#{members.count}) + .panel-head-actions + = link_to group_group_members_path(@group), class: 'btn btn-sm' do + %i.fa.fa-pencil-square-o + Edit group members + %ul.well-list + - members.each do |member| + = render 'groups/group_members/group_member', member: member, show_controls: false + - if members.count > 20 + %li + and #{members.count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(@group)} diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml new file mode 100644 index 00000000000..d708b01a114 --- /dev/null +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -0,0 +1,18 @@ += form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f| + .form-group + = f.label :user_ids, "People", class: 'control-label' + .col-sm-10 + = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true) + .help-block + Search for existing users or invite new ones using their email address. + + .form-group + = f.label :access_level, "Project Access", class: 'control-label' + .col-sm-10 + = select_tag :access_level, options_for_select(ProjectMember.access_roles, @project_member.access_level), class: "project-access-select select2" + .help-block + Read more about role permissions + %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + + .form-actions + = f.submit 'Add users to project', class: "btn btn-create" diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml new file mode 100644 index 00000000000..860a997cff8 --- /dev/null +++ b/app/views/projects/project_members/_project_member.html.haml @@ -0,0 +1,54 @@ +- user = member.user +- return unless user || member.invite? + +%li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)} + %span.list-item-name + - if member.user + = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: '' + %strong + = link_to user.name, user_path(user) + %span.cgray= user.username + - if user == current_user + %span.label.label-success It's you + - if user.blocked? + %label.label.label-danger + %strong Blocked + - else + = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: '' + %strong + = member.invite_email + %span.cgray + invited + - if member.created_by + by + = link_to member.created_by.name, user_path(member.created_by) + = time_ago_with_tooltip(member.created_at) + + - if current_user_can_admin_project + = link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do + Resend invite + + - if current_user_can_admin_project + - unless @project.personal? && user == current_user + .pull-right + %strong= member.human_access + = button_tag class: "btn-xs btn js-toggle-button", + title: 'Edit access level', type: 'button' do + %i.fa.fa-pencil-square-o + + + - if current_user == user + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do + = icon("sign-out") + 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 + + .edit-member.hide.js-toggle-content + %br + = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member), remote: true do |f| + .prepend-top-10 + = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control' + .prepend-top-10 + = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml new file mode 100644 index 00000000000..615c425e59a --- /dev/null +++ b/app/views/projects/project_members/_team.html.haml @@ -0,0 +1,11 @@ +- can_admin_project = can?(current_user, :admin_project, @project) + +.panel.panel-default.prepend-top-20 + .panel-heading + %strong #{@project.name} + project members + %small + (#{members.count}) + %ul.well-list + - members.each do |project_member| + = render 'project_member', member: project_member, current_user_can_admin_project: can_admin_project diff --git a/app/views/projects/team_members/import.html.haml b/app/views/projects/project_members/import.html.haml index d1f46c61b2e..6914543f6da 100644 --- a/app/views/projects/team_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -1,14 +1,15 @@ +- page_title "Import members" %h3.page-title Import members from another project %p.light Only project members will be imported. Group members will be skipped. %hr -= form_tag apply_import_project_team_members_path(@project), method: 'post', class: 'form-horizontal' do += form_tag apply_import_namespace_project_project_members_path(@project.namespace, @project), method: 'post', class: 'form-horizontal' do .form-group = label_tag :source_project_id, "Project", class: 'control-label' .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(current_user.authorized_projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true) .form-actions = button_tag 'Import project members', class: "btn btn-create" - = link_to "Cancel", project_team_index_path(@project), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml new file mode 100644 index 00000000000..162583e4b1d --- /dev/null +++ b/app/views/projects/project_members/index.html.haml @@ -0,0 +1,36 @@ +- page_title "Members" +%h3.page-title + Users with access to this project + +%p.light + Read more about project permissions + %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + +%hr + +.clearfix.js-toggle-container + = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' } + = button_tag 'Search', class: 'btn' + + - if can?(current_user, :admin_project_member, @project) + %span.pull-right + = button_tag class: 'btn btn-new btn-grouped js-toggle-button', type: 'button' do + Add members + %i.fa.fa-chevron-down + = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do + Import members + + .js-toggle-content.hide.new-group-member-holder + = render "new_project_member" + += render "team", members: @project_members + +- if @group + = render "group_members", members: @group_members + +:coffeescript + $('form.member-search-form').on 'submit', (event) -> + event.preventDefault() + Turbolinks.visit @.action + '?' + $(@).serialize() diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml new file mode 100644 index 00000000000..811b1858821 --- /dev/null +++ b/app/views/projects/project_members/update.js.haml @@ -0,0 +1,3 @@ +- can_admin_project = can?(current_user, :admin_project, @project) +:plain + $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member, current_user_can_admin_project: can_admin_project))}'); diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml new file mode 100644 index 00000000000..bb49f4de873 --- /dev/null +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -0,0 +1,34 @@ +- unless @branches.empty? + %br + %h4 Already Protected: + %table.table.protected-branches-list + %thead + %tr.no-border + %th Branch + %th Developers can push + %th Last commit + %th + + %tbody + - @branches.each do |branch| + - @url = namespace_project_protected_branch_path(@project.namespace, @project, branch) + %tr + %td + = link_to namespace_project_commits_path(@project.namespace, @project, branch.name) do + %strong= branch.name + - if @project.root_ref?(branch.name) + %span.label.label-info default + %td + = check_box_tag "developers_can_push", branch.id, branch.developers_can_push, "data-url" => @url + %td + - if commit = branch.commit + = link_to namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id' do + = commit.short_id + · + #{time_ago_with_tooltip(commit.committed_date)} + - else + (branch was removed from repository) + %td + .pull-right + - if can? current_user, :admin_project, @project + = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 227a2f9a061..52b3a50c1e6 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -1,8 +1,9 @@ +- page_title "Protected branches" %h3.page-title Protected branches %p.light Keep stable branches secure and force developers to use Merge Requests %hr -.bs-callout.bs-callout-info +.well.append-bottom-20 %p Protected branches are designed to %ul %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} @@ -11,7 +12,7 @@ %p Read more about #{link_to "project permissions", help_page_path("permissions", "permissions"), class: "underlined-link"} - if can? current_user, :admin_project, @project - = form_for [@project, @protected_branch], html: { class: 'form-horizontal' } do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'form-horizontal' } do |f| -if @protected_branch.errors.any? .alert.alert-danger %ul @@ -22,29 +23,14 @@ = f.label :name, "Branch", class: 'control-label' .col-sm-10 = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "select2"}) + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :developers_can_push do + = f.check_box :developers_can_push + %strong Developers can push + .help-block Allow developers to push to this branch .form-actions = f.submit 'Protect', class: "btn-create btn" -- unless @branches.empty? - %h5 Already Protected: - %ul.bordered-list.protected-branches-list - - @branches.each do |branch| - %li - %h4 - = link_to project_commits_path(@project, branch.name) do - %strong= branch.name - - if @project.root_ref?(branch.name) - %span.label.label-info default - %span.label.label-success - %i.fa.fa-lock - .pull-right - - if can? current_user, :admin_project, @project - = link_to 'Unprotect', [@project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-small" += render 'branches_list' - - if commit = branch.commit - = link_to project_commit_path(@project, commit.id), class: 'commit_short_id' do - = commit.short_id - %span.light - = gfm escape_once(truncate(commit.title, length: 40)) - #{time_ago_with_tooltip(commit.committed_date)} - - else - (branch was removed from repository) diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index 948a21aa816..35c15cf3a9e 100644 --- a/app/views/projects/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -11,9 +11,9 @@ - if @logs.present? :plain var current_url = location.href.replace(/\/?$/, '/'); - var log_url = '#{project_tree_url(@project, tree_join(@ref, @path || '/'))}'.replace(/\/?$/, '/'); + var log_url = '#{namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/'))}'.replace(/\/?$/, '/'); if(current_url == log_url) { // Load 10 more commit log for each file in tree // if we still on the same page - ajaxGet('#{logs_file_project_ref_path(@project, @ref, @path || '/', offset: (@offset + @limit))}'); + ajaxGet('#{logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit))}'); } diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index ce69adeb48c..b9486a9b492 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -3,35 +3,35 @@ - split_button = split_button || false - if split_button == true %span.btn-group{class: btn_class} - = link_to archive_project_repository_path(@project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do %i.fa.fa-download %span Download zip - %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } %span.caret %span.sr-only Select Archive Format - %ul.dropdown-menu{ role: 'menu' } + %ul.col-xs-10.dropdown-menu{ role: 'menu' } %li - = link_to archive_project_repository_path(@project, ref: ref, format: 'zip'), rel: 'nofollow' do + = 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_project_repository_path(@project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + = 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 %li - = link_to archive_project_repository_path(@project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do %i.fa.fa-download %span Download tar.bz2 %li - = link_to archive_project_repository_path(@project, ref: ref, format: 'tar'), rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do %i.fa.fa-download %span Download tar - else %span.btn-group{class: btn_class} - = link_to archive_project_repository_path(@project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do %i.fa.fa-download %span zip - = link_to archive_project_repository_path(@project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do %i.fa.fa-download %span tar.gz diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml index c77ffff43fe..f3526ad0747 100644 --- a/app/views/projects/repositories/_feed.html.haml +++ b/app/views/projects/repositories/_feed.html.haml @@ -1,7 +1,7 @@ - commit = update %tr %td - = link_to project_commits_path(@project, commit.head.name) do + = link_to namespace_project_commits_path(@project.namespace, @project, commit.head.name) do %strong = commit.head.name - if @project.root_ref?(commit.head.name) @@ -9,7 +9,7 @@ %td %div - = link_to project_commits_path(@project, commit.id) do + = 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: '' = gfm escape_once(truncate(commit.title, length: 40)) diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 1151f22c7e8..e1823b51198 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -5,51 +5,17 @@ %p= @service.description .back-link - = link_to project_services_path(@project) do + = link_to namespace_project_services_path(@project.namespace, @project) do ← to services %hr -= form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| - - if @service.errors.any? - .alert.alert-danger - %ul - - @service.errors.full_messages.each do |msg| - %li= msg - - - if @service.help.present? - .bs-callout - = @service.help - - .form-group - = f.label :active, "Active", class: "control-label" - .col-sm-10 - = f.check_box :active - - - @service.fields.each do |field| - - name = field[:name] - - value = @service.send(name) unless field[:type] == 'password' - - type = field[:type] - - placeholder = field[:placeholder] - - choices = field[:choices] - - default_choice = field[:default_choice] - - .form-group - = f.label name, class: "control-label" - .col-sm-10 - - if type == 'text' - = f.text_field name, class: "form-control", placeholder: placeholder - - elsif type == 'textarea' - = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder - - elsif type == 'checkbox' - = f.check_box name - - elsif type == 'select' - = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - - elsif type == 'password' - = f.password_field name, class: 'form-control' += 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 - = f.submit 'Save', class: 'btn btn-save' + = form.submit 'Save', class: 'btn btn-save' - - if @service.valid? && @service.activated? && @service.can_test? - = link_to 'Test settings', test_project_service_path(@project, @service.to_param), class: 'btn' + - 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}" diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml index bcc5832792f..50ed78286d2 100644 --- a/app/views/projects/services/edit.html.haml +++ b/app/views/projects/services/edit.html.haml @@ -1 +1,2 @@ +- page_title @service.title, "Services" = render 'form' diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml index 7271dd830ca..1065def693b 100644 --- a/app/views/projects/services/index.html.haml +++ b/app/views/projects/services/index.html.haml @@ -1,13 +1,23 @@ +- page_title "Services" %h3.page-title Project services %p.light Project services allow you to integrate GitLab with other applications -%hr -%ul.bordered-list +%table.table + %thead + %tr + %th + %th Service + %th Description + %th Last edit - @services.sort_by(&:title).each do |service| - %li - %h4 - = link_to edit_project_service_path(@project, service.to_param) do - = service.title - .pull-right - = boolean_to_icon service.activated? - %p= service.description + %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 diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder new file mode 100644 index 00000000000..242684e5c7c --- /dev/null +++ b/app/views/projects/show.atom.builder @@ -0,0 +1,12 @@ +xml.instruct! +xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do + xml.title "#{@project.name} activity" + xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" + xml.id namespace_project_url(@project.namespace, @project) + xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + + @events.each do |event| + event_to_atom(xml, event) + end +end diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 9b06ebe95a4..2259dea0865 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,83 +1,16 @@ -= render "home_panel" - -- readme = @repository.readme -%ul.nav.nav-tabs - %li.active - = link_to '#tab-activity', 'data-toggle' => 'tab' do - Activity - - if readme - %li - = link_to '#tab-readme', 'data-toggle' => 'tab' do - Readme - .project-home-links - - unless @project.empty_repo? - = link_to pluralize(number_with_delimiter(@repository.commit_count), 'commit'), project_commits_path(@project, @ref || @repository.root_ref) - = link_to pluralize(number_with_delimiter(@repository.branch_names.count), 'branch'), project_branches_path(@project) - = link_to pluralize(number_with_delimiter(@repository.tag_names.count), 'tag'), project_tags_path(@project) - %span.light.prepend-left-20= repository_size - -.tab-content - .tab-pane.active#tab-activity - .row - %section.col-md-9 - = render "events/event_last_push", event: @last_push - = render 'shared/event_filter' - .content_list - = spinner - %aside.col-md-3.project-side.hidden-sm.hidden-xs - .clearfix - - if @project.archived? - .alert.alert-warning - %h4 - %i.fa.fa-exclamation-triangle - Archived project! - %p Repository is read-only - - - if @project.forked_from_project - .alert.alert-success - %i.fa.fa-code-fork.project-fork-icon - Forked from: - %br - = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity") - - unless @project.empty_repo? - = link_to project_compare_index_path(@project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-block' do - Compare code +- if current_user && can?(current_user, :download_code, @project) + = render 'shared/no_ssh' + = render 'shared/no_password' - - if @repository.version - - version = @repository.version - = link_to project_blob_path(@project, tree_join(@repository.root_ref, version.name)), class: 'btn btn-block' do - Version: - %span.count - = @repository.blob_by_oid(version.id).data - - .prepend-top-10 - %p - %span.light Created on - #{@project.created_at.stamp('Aug 22, 2013')} - %p - %span.light Owned by - - if @project.group - #{link_to @project.group.name, @project.group} group - - else - #{link_to @project.owner_name, @project.owner} - - - @project.ci_services.each do |ci_service| - - if ci_service.active? && ci_service.respond_to?(:builds_path) - - if ci_service.respond_to?(:status_img_path) - = link_to ci_service.builds_path do - = image_tag ci_service.status_img_path, alt: "build status" - - else - %span.light CI provided by - = link_to ci_service.title, ci_service.builds_path - - - if readme - .tab-pane#tab-readme - %article.readme-holder#README - = link_to project_blob_path(@project, tree_join(@repository.root_ref, readme.name)) do - %h4.readme-file-title - %i.fa.fa-file - = readme.name - .wiki - = render_readme(readme) += render "home_panel" += render 'shared/show_aside' +.row + %section.col-md-8 + = render 'section' + %aside.col-md-4.project-side + = render 'aside' diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index f6a5bf9e4ff..945f0084dff 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -1,4 +1,5 @@ +- page_title "Edit", @snippet.title, "Snippets" %h3.page-title Edit snippet %hr -= render "shared/snippets/form", url: project_snippet_path(@project, @snippet) += render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index e60f9a44322..da9401bd8c1 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,7 +1,8 @@ +- page_title "Snippets" %h3.page-title Snippets - if can? current_user, :write_project_snippet, @project - = link_to new_project_snippet_path(@project), class: "btn btn-new pull-right", title: "New Snippet" do + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Snippet" do Add new snippet %p.light diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index 10f684b6316..e38d95c45e7 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -1,4 +1,5 @@ +- page_title "New Snippets" %h3.page-title New snippet %hr -= render "shared/snippets/form", url: project_snippets_path(@project, @snippet) += render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index ada0d30c496..5725d804df3 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -1,8 +1,9 @@ +- page_title @snippet.title, "Snippets" %h3.page-title = @snippet.title .pull-right - = link_to new_project_snippet_path(@project), class: "btn btn-new", title: "New Snippet" do + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do Add new snippet %hr @@ -17,21 +18,21 @@ = @snippet.author_name .back-link - = link_to project_snippets_path(@project) do + = link_to namespace_project_snippets_path(@project.namespace, @project) do ← project snippets .file-holder .file-title %i.fa.fa-file - %span.file_name + %strong = @snippet.file_name - .options + .file-actions .btn-group - if can?(current_user, :modify_project_snippet, @snippet) - = link_to "edit", edit_project_snippet_path(@project, @snippet), class: "btn btn-small", title: 'Edit Snippet' - = link_to "raw", raw_project_snippet_path(@project, @snippet), class: "btn btn-small", target: "_blank" + = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", title: 'Edit Snippet' + = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - if can?(current_user, :admin_project_snippet, @snippet) - = link_to "remove", project_snippet_path(@project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-small btn-remove", title: 'Delete Snippet' + = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet' = render 'shared/snippets/blob' %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 4ab102ba96c..28ad272322f 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -1,7 +1,7 @@ - commit = @repository.commit(tag.target) %li %h4 - = link_to project_commits_path(@project, tag.name), class: "" do + = link_to namespace_project_commits_path(@project.namespace, @project, tag.name), class: "" do %i.fa.fa-tag = tag.name - if tag.message.present? @@ -9,9 +9,9 @@ = strip_gpg_signature(tag.message) .pull-right - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-small' + = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-xs' - if can?(current_user, :admin_project, @project) - = link_to project_tag_path(@project, tag.name), class: 'btn btn-small btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-xs btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do %i.fa.fa-trash-o - if commit diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index ac74e3b6d36..d4652a47cba 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,10 +1,11 @@ +- page_title "Tags" = render "projects/commits/head" %h3.page-title Git Tags - if can? current_user, :push_code, @project .pull-right - = link_to new_project_tag_path(@project), class: 'btn btn-create new-tag-btn' do + = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do %i.fa.fa-add-sign New tag diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index ad7ff8d3db8..172fafdeeff 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Tag" - if @error .alert.alert-danger %button{ type: "button", class: "close", "data-dismiss" => "alert"} × @@ -5,7 +6,7 @@ %h3.page-title %i.fa.fa-code-fork New tag -= form_tag project_tags_path, method: :post, class: "form-horizontal" do += form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal" do .form-group = label_tag :tag_name, 'Name for new tag', class: 'control-label' .col-sm-10 @@ -22,9 +23,10 @@ .light (Optional) Entering a message will create an annotated tag. .form-actions = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 - = link_to 'Cancel', project_tags_path(@project), class: 'btn btn-cancel' + = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' :javascript + disableButtonIfAnyEmptyField($("#new-tag-form"), ".form-control", ".btn-create"); var availableTags = #{@project.repository.ref_names.to_json}; $("#ref").autocomplete({ diff --git a/app/views/projects/team_members/_form.html.haml b/app/views/projects/team_members/_form.html.haml deleted file mode 100644 index 2bf61fa12bb..00000000000 --- a/app/views/projects/team_members/_form.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -%h3.page-title - New project member(s) - -= form_for @user_project_relation, as: :project_member, url: project_team_members_path(@project), html: { class: "form-horizontal users-project-form" } do |f| - -if @user_project_relation.errors.any? - .alert.alert-danger - %ul - - @user_project_relation.errors.full_messages.each do |msg| - %li= msg - - %p 1. Choose people you want in the project - .form-group - = f.label :user_ids, "People", class: 'control-label' - .col-sm-10 - = users_select_tag(:user_ids, multiple: true) - - %p 2. Set access level for them - .form-group - = f.label :access_level, "Project Access", class: 'control-label' - .col-sm-10= select_tag :access_level, options_for_select(Gitlab::Access.options, @user_project_relation.access_level), class: "project-access-select select2" - - .form-actions - = f.submit 'Add users', class: "btn btn-create" - = link_to "Cancel", project_team_index_path(@project), class: "btn btn-cancel" diff --git a/app/views/projects/team_members/_group_members.html.haml b/app/views/projects/team_members/_group_members.html.haml deleted file mode 100644 index df3c914fdea..00000000000 --- a/app/views/projects/team_members/_group_members.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- group_users_count = @group.group_members.count -.panel.panel-default - .panel-heading - %strong #{@group.name} - group members (#{group_users_count}) - .pull-right - = link_to members_group_path(@group), class: 'btn btn-small' do - %i.fa.fa-pencil-square-o - %ul.well-list - - @group.group_members.order('access_level DESC').limit(20).each do |member| - = render 'groups/group_members/group_member', member: member, show_controls: false - - if group_users_count > 20 - %li - and #{group_users_count - 20} more. For full list visit #{link_to 'group members page', members_group_path(@group)} diff --git a/app/views/projects/team_members/_team.html.haml b/app/views/projects/team_members/_team.html.haml deleted file mode 100644 index 0e5b8176132..00000000000 --- a/app/views/projects/team_members/_team.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -.team-table - - can_admin_project = (can? current_user, :admin_project, @project) - .panel.panel-default - .panel-heading - %strong #{@project.name} - project members (#{members.count}) - %ul.well-list - - members.each do |team_member| - = render 'team_member', member: team_member, current_user_can_admin_project: can_admin_project diff --git a/app/views/projects/team_members/_team_member.html.haml b/app/views/projects/team_members/_team_member.html.haml deleted file mode 100644 index 7a9c0939ba0..00000000000 --- a/app/views/projects/team_members/_team_member.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- user = member.user -%li{id: dom_id(user), class: "team_member_row access-#{member.human_access.downcase}"} - .pull-right - - if current_user_can_admin_project - - unless @project.personal? && user == current_user - .pull-left - = form_for(member, as: :project_member, url: project_team_member_path(@project, member.user)) do |f| - = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: "trigger-submit" - - = link_to project_team_member_path(@project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from team' do - %i.fa.fa-minus.fa-inverse - = image_tag avatar_icon(user.email, 32), class: "avatar s32" - %p - %strong= user.name - %span.cgray= user.username - - diff --git a/app/views/projects/team_members/index.html.haml b/app/views/projects/team_members/index.html.haml deleted file mode 100644 index ecb7c689e8a..00000000000 --- a/app/views/projects/team_members/index.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -%h3.page-title - Users with access to this project - - - if can? current_user, :admin_team_member, @project - %span.pull-right - = link_to new_project_team_member_path(@project), class: "btn btn-new btn-grouped", title: "New project member" do - New project member - = link_to import_project_team_members_path(@project), class: "btn btn-grouped", title: "Import members from another project" do - Import members - -%p.light - Read more about project permissions - %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" -= render "team", members: @project_members -- if @group - = render "group_members" diff --git a/app/views/projects/team_members/new.html.haml b/app/views/projects/team_members/new.html.haml deleted file mode 100644 index b1bc3ba0eba..00000000000 --- a/app/views/projects/team_members/new.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render "form" diff --git a/app/views/projects/team_members/update.js.haml b/app/views/projects/team_members/update.js.haml deleted file mode 100644 index c68fe9574a2..00000000000 --- a/app/views/projects/team_members/update.js.haml +++ /dev/null @@ -1,6 +0,0 @@ -- if @user_project_relation.valid? - :plain - $("##{dom_id(@user_project_relation)}").effect("highlight", {color: "#529214"}, 1000);; -- else - :plain - $("##{dom_id(@user_project_relation)}").effect("highlight", {color: "#D12F19"}, 1000);; diff --git a/app/views/projects/transfer.js.haml b/app/views/projects/transfer.js.haml index 10b0de98c04..17b9fecfeb1 100644 --- a/app/views/projects/transfer.js.haml +++ b/app/views/projects/transfer.js.haml @@ -1,7 +1,2 @@ -- if @project.errors[:namespace_id].present? - :plain - $("#tab-transfer .errors-holder").replaceWith(errorMessage('#{escape_javascript(@project.errors[:namespace_id].first)}')); - $("#tab-transfer .form-actions input").removeAttr('disabled').removeClass('disabled'); -- else - :plain - location.href = "#{edit_project_path(@project)}"; +:plain + location.href = "#{edit_namespace_project_path(@project.namespace, @project)}"; diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml index 393ef0e24bd..02ecbade219 100644 --- a/app/views/projects/tree/_blob_item.html.haml +++ b/app/views/projects/tree/_blob_item.html.haml @@ -1,8 +1,8 @@ %tr{ class: "tree-item #{tree_hex_class(blob_item)}" } %td.tree-item-file-name - = tree_icon(type) + = tree_icon(type, blob_item.mode, blob_item.name) %span.str-truncated - = link_to blob_item.name, project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)) + = link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)) %td.tree_time_ago.cgray = render 'spinner' %td.hidden-xs.tree_commit diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml index 46e9be4af83..2b5f671c09e 100644 --- a/app/views/projects/tree/_submodule_item.html.haml +++ b/app/views/projects/tree/_submodule_item.html.haml @@ -1,14 +1,6 @@ -- tree, commit = submodule_links(submodule_item) %tr{ class: "tree-item" } %td.tree-item-file-name - %i.fa.fa-archive - %span - = link_to truncate(submodule_item.name, length: 40), tree - @ - %span.monospace - - if commit.nil? - #{truncate_sha(submodule_item.id)} - - else - = link_to "#{truncate_sha(submodule_item.id)}", commit + %i.fa.fa-archive.fa-fw + = submodule_link(submodule_item, @ref) %td %td.hidden-xs diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml index 1159fcadffd..d304690d162 100644 --- a/app/views/projects/tree/_tree.html.haml +++ b/app/views/projects/tree/_tree.html.haml @@ -1,16 +1,16 @@ %ul.breadcrumb.repo-breadcrumb %li - = link_to project_tree_path(@project, @ref) do + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do = @project.path - tree_breadcrumbs(tree, 6) do |title, path| %li - if path - = link_to truncate(title, length: 40), project_tree_path(@project, path) + = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) - else = link_to title, '#' - if current_user && can_push_branch?(@project, @ref) %li - = link_to project_new_tree_path(@project, @id), title: 'New file', id: 'new-file-link' do + = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'New file', id: 'new-file-link' do %small %i.fa.fa-plus @@ -27,15 +27,15 @@ %i.fa.fa-angle-right %small.light - = link_to @commit.short_id, project_commit_path(@project, @commit) + = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit) – = truncate(@commit.title, length: 50) - = link_to 'History', project_commits_path(@project, @id), class: 'pull-right' + = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' - if @path.present? %tr.tree-item %td.tree-item-file-name - = link_to "..", project_tree_path(@project, up_dir_path(tree)), class: 'prepend-left-10' + = link_to "..", namespace_project_tree_path(@project.namespace, @project, up_dir_path), class: 'prepend-left-10' %td %td.hidden-xs diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index bd50dd4d9a2..50521264a61 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,3 +1,3 @@ %span.str-truncated %span.tree_author= commit_author_link(commit, avatar: true, size: 16) - = link_to_gfm commit.title, project_commit_path(@project, commit.id), class: "tree-commit-link" + = link_to_gfm commit.title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link" diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index f8cecf9be1f..e87138bf980 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -1,8 +1,9 @@ %tr{ class: "tree-item #{tree_hex_class(tree_item)}" } %td.tree-item-file-name - = tree_icon(type) + = tree_icon(type, tree_item.mode, tree_item.name) %span.str-truncated - = link_to tree_item.name, project_tree_path(@project, tree_join(@id || @commit.id, tree_item.name)) + - path = flatten_tree(tree_item) + = link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)) %td.tree_time_ago.cgray = render 'spinner' %td.hidden-xs.tree_commit diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index fc4616da6ec..04590f65b27 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -1,9 +1,14 @@ +- page_title @path.presence || "Files", @ref += 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") + .tree-ref-holder = render 'shared/ref_switcher', destination: 'tree', path: @path - if can? current_user, :download_code, @project .tree-download-holder - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group-small pull-right hidden-xs hidden-sm', split_button: true + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true #tree-holder.tree-holder.clearfix = render "tree", tree: @tree diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml index cbb21f2b9fb..7d9bd08385a 100644 --- a/app/views/projects/update.js.haml +++ b/app/views/projects/update.js.haml @@ -1,9 +1,9 @@ - if @project.valid? :plain - location.href = "#{edit_project_path(@project)}"; + location.href = "#{edit_namespace_project_path(@project.namespace, @project)}"; - else :plain $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); $('.save-project-loader').hide(); $('.project-edit-container').show(); - $('.project-edit-content .btn-save').enableButton(); + $('.project-edit-content .btn-save').enable(); diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index f37c086716d..904600499ae 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form' } do |f| -if @page.errors.any? #error_explanation .alert.alert-danger @@ -12,20 +12,21 @@ = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: "form-control" .row - .col-sm-2 - .col-sm-10 + .col-sm-offset-2.col-sm-10 %p.cgray To link to a (new) page you can just type %code [Link Title](page-slug) \. - .form-group + .form-group.wiki-content = f.label :content, class: 'control-label' .col-sm-10 - = render 'projects/zen', f: f, attr: :content, classes: 'description form-control' - .col-sm-12.hint - .pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'} - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do + = render 'projects/zen', f: f, attr: :content, classes: 'description form-control' + .col-sm-12.hint + .pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'} + .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .clearfix .error-alert .form-group @@ -35,11 +36,7 @@ .form-actions - if @page && @page.persisted? = f.submit 'Save changes', class: "btn-save btn" - = link_to "Cancel", project_wiki_path(@project, @page), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel" - else = f.submit 'Create page', class: "btn-create btn" - = link_to "Cancel", project_wiki_path(@project, :home), class: "btn btn-cancel" - -:javascript - window.project_image_path_upload = "#{upload_image_project_path @project}"; - + = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel" diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 30410bc95e0..633214a4e86 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,8 +1,8 @@ %span.pull-right - if (@page && @page.persisted?) - = link_to history_project_wiki_path(@project, @page), class: "btn btn-grouped" do + = link_to history_namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-grouped" do Page History - if can?(current_user, :write_wiki, @project) - = link_to edit_project_wiki_path(@project, @page), class: "btn btn-grouped" do + = link_to edit_namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-grouped" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index 90539fde583..693c3facb32 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -1,12 +1,12 @@ %ul.nav.nav-tabs = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do - = link_to 'Home', project_wiki_path(@project, :home) + = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) = nav_link(path: 'wikis#pages') do - = link_to 'Pages', pages_project_wikis_path(@project) + = link_to 'Pages', pages_namespace_project_wikis_path(@project.namespace, @project) = nav_link(path: 'wikis#git_access') do - = link_to git_access_project_wikis_path(@project) do + = link_to git_access_namespace_project_wikis_path(@project.namespace, @project) do %i.fa.fa-download Git Access diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index 1ce292a02df..dace172438c 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -1,4 +1,4 @@ -%div#modal-new-wiki.modal.hide +%div#modal-new-wiki.modal .modal-dialog .modal-content .modal-header @@ -7,7 +7,9 @@ .modal-body = label_tag :new_wiki_path do %span Page slug - = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project) + = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project) + %p.hidden.text-danger{data: { error: "slug" }} + The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and / %p.hint Please don't use spaces. .modal-footer diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 5347caf000a..3f1dce1050c 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "Edit", @page.title, "Wiki" = render 'nav' .pull-right = render 'main_links' @@ -9,5 +10,5 @@ .pull-right - if @page.persisted? && can?(current_user, :admin_wiki, @project) - = link_to project_wiki_path(@project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-small btn-remove" do + = 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-sm btn-remove" do Delete this page diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml index 48058124f97..ead99412406 100644 --- a/app/views/projects/wikis/empty.html.haml +++ b/app/views/projects/wikis/empty.html.haml @@ -1,3 +1,4 @@ +- page_title "Wiki" %h3.page-title Empty page %hr .error_message diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 365edb524f4..825f2a161c4 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,3 +1,4 @@ +- page_title "Git Access", "Wiki" = render 'nav' .row .col-sm-6 diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index ef4b8f74714..673ec2d20e5 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,7 +1,8 @@ +- page_title "History", @page.title, "Wiki" = render 'nav' %h3.page-title %span.light History for - = link_to @page.title, project_wiki_path(@project, @page) + = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page) %table.table %thead @@ -12,18 +13,19 @@ %th Last updated %th Format %tbody - - @page.versions.each do |version| + - @page.versions.each_with_index do |version, index| - commit = version %tr %td - = link_to project_wiki_path(@project, @page, version_id: commit.id) do + = link_to project_wiki_path_with_version(@project, @page, + commit.id, index == 0) do = truncate_sha(commit.id) %td = commit.author.name %td = commit.message %td - #{time_ago_with_tooltip(version.date)} + #{time_ago_with_tooltip(version.authored_date)} %td %strong = @page.page.wiki.page(@page.page.name, commit.id).try(:format) diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 264b48ec36c..890ff1aed73 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,3 +1,4 @@ +- page_title "All Pages", "Wiki" = render 'nav' %h3.page-title All Pages @@ -5,7 +6,7 @@ - @wiki_pages.each do |wiki_page| %li %h4 - = link_to wiki_page.title, project_wiki_path(@project, wiki_page) + = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) %small (#{wiki_page.format}) .pull-right %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index ede4fef9e24..83cd4c66672 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,3 +1,4 @@ +- page_title @page.title, "Wiki" = render 'nav' %h3.page-title = @page.title @@ -5,7 +6,7 @@ - if @page.historical? .warning_message This is an old version of this page. - You can view the #{link_to "most recent version", project_wiki_path(@project, @page)} or browse the #{link_to "history", history_project_wiki_path(@project, @page)}. + You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", history_namespace_project_wiki_path(@project.namespace, @project, @page)}. %hr diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml new file mode 100644 index 00000000000..154332cb9a9 --- /dev/null +++ b/app/views/search/_category.html.haml @@ -0,0 +1,77 @@ +%ul.nav.nav-pills.search-filter + - if @project + %li{class: ("active" if @scope == 'blobs')} + = link_to search_filter_path(scope: 'blobs') do + = icon('code fw') + %span + Code + %span.badge + = @search_results.blobs_count + %li{class: ("active" if @scope == 'issues')} + = link_to search_filter_path(scope: 'issues') do + = icon('exclamation-circle fw') + %span + Issues + %span.badge + = @search_results.issues_count + %li{class: ("active" if @scope == 'merge_requests')} + = link_to search_filter_path(scope: 'merge_requests') do + = icon('tasks fw') + %span + Merge requests + %span.badge + = @search_results.merge_requests_count + %li{class: ("active" if @scope == 'notes')} + = link_to search_filter_path(scope: 'notes') do + = icon('comments fw') + %span + Comments + %span.badge + = @search_results.notes_count + %li{class: ("active" if @scope == 'wiki_blobs')} + = link_to search_filter_path(scope: 'wiki_blobs') do + = icon('book fw') + %span + Wiki + %span.badge + = @search_results.wiki_blobs_count + + - elsif @show_snippets + %li{class: ("active" if @scope == 'snippet_blobs')} + = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do + = icon('code fw') + %span + Snippet Contents + %span.badge + = @search_results.snippet_blobs_count + %li{class: ("active" if @scope == 'snippet_titles')} + = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do + = icon('book fw') + %span + Titles and Filenames + %span.badge + = @search_results.snippet_titles_count + + - else + %li{class: ("active" if @scope == 'projects')} + = link_to search_filter_path(scope: 'projects') do + = icon('bookmark fw') + %span + Projects + %span.badge + = @search_results.projects_count + %li{class: ("active" if @scope == 'issues')} + = link_to search_filter_path(scope: 'issues') do + = icon('exclamation-circle fw') + %span + Issues + %span.badge + = @search_results.issues_count + %li{class: ("active" if @scope == 'merge_requests')} + = link_to search_filter_path(scope: 'merge_requests') do + = icon('tasks fw') + %span + Merge requests + %span.badge + = @search_results.merge_requests_count + diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index eca69ce50b1..e2d0cab9e79 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -1,5 +1,5 @@ .dropdown.inline - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle.btn.btn{type: 'button', 'data-toggle' => 'dropdown'} %i.fa.fa-tags %span.light Group: - if @group.present? @@ -17,7 +17,7 @@ = group.name .dropdown.inline.prepend-left-10.project-filter - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle.btn.btn{type: 'button', 'data-toggle' => 'dropdown'} %i.fa.fa-tags %span.light Project: - if @project.present? diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml new file mode 100644 index 00000000000..5ee70be1ad6 --- /dev/null +++ b/app/views/search/_form.html.haml @@ -0,0 +1,12 @@ += form_tag search_path, method: :get, class: 'form-inline' do |f| + = hidden_field_tag :project_id, params[:project_id] + = hidden_field_tag :group_id, params[:group_id] + = hidden_field_tag :snippets, params[:snippets] + = hidden_field_tag :scope, params[:scope] + .search-holder.clearfix + .form-group + = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input", id: "dashboard_search", autofocus: true + = button_tag 'Search', class: "btn btn-primary" + - unless params[:snippets].eql? 'true' + .pull-right + = render 'filter' diff --git a/app/views/search/_global_filter.html.haml b/app/views/search/_global_filter.html.haml deleted file mode 100644 index 442bd84f930..00000000000 --- a/app/views/search/_global_filter.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -%ul.nav.nav-pills.nav-stacked.search-filter - %li{class: ("active" if @scope == 'projects')} - = link_to search_filter_path(scope: 'projects') do - Projects - .pull-right - = @search_results.projects_count - %li{class: ("active" if @scope == 'issues')} - = link_to search_filter_path(scope: 'issues') do - Issues - .pull-right - = @search_results.issues_count - %li{class: ("active" if @scope == 'merge_requests')} - = link_to search_filter_path(scope: 'merge_requests') do - Merge requests - .pull-right - = @search_results.merge_requests_count diff --git a/app/views/search/_project_filter.html.haml b/app/views/search/_project_filter.html.haml deleted file mode 100644 index ad933502a28..00000000000 --- a/app/views/search/_project_filter.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -%ul.nav.nav-pills.nav-stacked.search-filter - %li{class: ("active" if @scope == 'blobs')} - = link_to search_filter_path(scope: 'blobs') do - %i.fa.fa-code - Code - .pull-right - = @search_results.blobs_count - %li{class: ("active" if @scope == 'issues')} - = link_to search_filter_path(scope: 'issues') do - %i.fa.fa-exclamation-circle - Issues - .pull-right - = @search_results.issues_count - %li{class: ("active" if @scope == 'merge_requests')} - = link_to search_filter_path(scope: 'merge_requests') do - %i.fa.fa-code-fork - Merge requests - .pull-right - = @search_results.merge_requests_count - %li{class: ("active" if @scope == 'notes')} - = link_to search_filter_path(scope: 'notes') do - %i.fa.fa-comments - Comments - .pull-right - = @search_results.notes_count - %li{class: ("active" if @scope == 'wiki_blobs')} - = link_to search_filter_path(scope: 'wiki_blobs') do - %i.fa.fa-book - Wiki - .pull-right - = @search_results.wiki_blobs_count - diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 58bcff9dbe3..741c780ad96 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -1,28 +1,21 @@ -%h4 - #{@search_results.total_count} results found - - unless @show_snippets - - if @project - for #{link_to @project.name_with_namespace, @project} - - elsif @group - for #{link_to @group.name, @group} +- if @search_results.empty? + = render partial: "search/results/empty" +- else + .light + Search results for + %code + = @search_term + - unless @show_snippets + - if @project + in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]} + - elsif @group + in group #{link_to @group.name, @group} -%hr - -.row - .col-sm-3 - - if @project - = render "project_filter" - - elsif @show_snippets - = render 'snippet_filter' - - else - = render "global_filter" - .col-sm-9 + %br + .results.prepend-top-10 .search-results - - if @search_results.empty? - = render partial: "search/results/empty", locals: { message: "We couldn't find any matching results" } - - else - = render partial: "search/results/#{@scope.singularize}", collection: @objects - = paginate @objects, theme: 'gitlab' + = render partial: "search/results/#{@scope.singularize}", collection: @objects + = paginate @objects, theme: 'gitlab' :javascript $(".search-results .term").highlight("#{escape_javascript(params[:search])}"); diff --git a/app/views/search/_snippet_filter.html.haml b/app/views/search/_snippet_filter.html.haml deleted file mode 100644 index 95d23fa9f47..00000000000 --- a/app/views/search/_snippet_filter.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -%ul.nav.nav-pills.nav-stacked.search-filter - %li{class: ("active" if @scope == 'snippet_blobs')} - = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do - %i.fa.fa-code - Snippet Contents - .pull-right - = @search_results.snippet_blobs_count - %li{class: ("active" if @scope == 'snippet_titles')} - = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do - %i.fa.fa-book - Titles and Filenames - .pull-right - = @search_results.snippet_titles_count diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index b46b4832e19..84e9be82c44 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,9 +1,9 @@ .blob-result .file-holder .file-title - = link_to project_blob_path(@project, tree_join(blob.ref, blob.filename), :anchor => "L" + blob.startline.to_s) do + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(blob.ref, blob.filename), :anchor => "L" + blob.startline.to_s) do %i.fa.fa-file %strong = blob.filename .file-content.code.term - = render 'shared/file_hljs', blob: blob, first_line_number: blob.startline + = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, user_color_scheme_class: 'white' diff --git a/app/views/search/results/_empty.html.haml b/app/views/search/results/_empty.html.haml index 01fb8cd9b8e..05a63016c09 100644 --- a/app/views/search/results/_empty.html.haml +++ b/app/views/search/results/_empty.html.haml @@ -1,4 +1,6 @@ .search_box .search_glyph - %span.fa.fa-search - %h4 #{message} + %h4 + = icon('search') + We couldn't find any results matching + %code #{@search_term} diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index 7868f958261..ce8ddff9556 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -1,6 +1,6 @@ .search-result-row %h4 - = link_to [issue.project, issue] do + = link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do %span.term.str-truncated= issue.title .pull-right ##{issue.iid} - if issue.description.present? diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 56b185283bd..adfdd1c7506 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -1,6 +1,6 @@ .search-result-row %h4 - = link_to [merge_request.target_project, merge_request] do + = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do %span.term.str-truncated= merge_request.title .pull-right ##{merge_request.iid} - if merge_request.description.present? @@ -11,6 +11,6 @@ #{merge_request.project.name_with_namespace} .pull-right - if merge_request.merged? - %span.label.label-primary Merged + %span.label.label-primary Accepted - elsif merge_request.closed? - %span.label.label-danger Closed + %span.label.label-danger Rejected diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index a44a4542df5..5fcba2b7e93 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -9,7 +9,7 @@ = link_to project do = project.name_with_namespace · - = link_to project_commit_path(project, note.commit_id, anchor: dom_id(note)) do + = link_to namespace_project_commit_path(project.namespace, project, note.commit_id, anchor: dom_id(note)) do Commit #{truncate_sha(note.commit_id)} - else = link_to project do @@ -17,7 +17,7 @@ · %span #{note.noteable_type.titleize} ##{note.noteable.iid} · - = link_to [project, note.noteable, anchor: dom_id(note)] do + = link_to [project.namespace.becomes(Namespace), project, note.noteable, anchor: dom_id(note)] do = note.noteable.title .note-search-result diff --git a/app/views/search/results/_project.html.haml b/app/views/search/results/_project.html.haml index 301b65eca29..195cf06c8ea 100644 --- a/app/views/search/results/_project.html.haml +++ b/app/views/search/results/_project.html.haml @@ -1,6 +1,6 @@ .search-result-row %h4 - = link_to project do + = link_to [project.namespace.becomes(Namespace), project] do %span.term= project.name_with_namespace - if project.description.present? %span.light.term= project.description diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 6fc2cdf6362..95099853918 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -13,22 +13,7 @@ .file-title %i.fa.fa-file %strong= snippet_blob[:snippet_object].file_name - %span.options - .btn-group.tree-btn-group.pull-right - - if snippet_blob[:snippet_object].author == current_user - = link_to "Edit", edit_snippet_path(snippet_blob[:snippet_object]), class: "btn btn-tiny", title: 'Edit Snippet' - = link_to "Delete", snippet_path(snippet_blob[:snippet_object]), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-tiny", title: 'Delete Snippet' - = link_to "Raw", raw_snippet_path(snippet_blob[:snippet_object]), class: "btn btn-tiny", target: "_blank" - - if gitlab_markdown?(snippet_blob[:snippet_object].file_name) - .file-content.wiki - - snippet_blob[:snippet_chunks].each do |snippet| - - unless snippet[:data].empty? - = preserve do - = markdown(snippet[:data]) - - else - .file-content.code - .nothing-here-block Empty file - - elsif markup?(snippet_blob[:snippet_object].file_name) + - if markup?(snippet_blob[:snippet_object].file_name) .file-content.wiki - snippet_blob[:snippet_chunks].each do |snippet| - unless snippet[:data].empty? diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index f7e5ee5e20e..c414acb6a11 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -11,7 +11,7 @@ %small.pull-right.cgray - if snippet_title.project_id? - = link_to snippet_title.project.name_with_namespace, project_path(snippet_title.project) + = link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project) .snippet-info = "##{snippet_title.id}" diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml index e361074b6a0..f9c5810e3d0 100644 --- a/app/views/search/results/_wiki_blob.html.haml +++ b/app/views/search/results/_wiki_blob.html.haml @@ -1,9 +1,9 @@ .blob-result .file-holder .file-title - = link_to project_wiki_path(@project, wiki_blob.filename) do + = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.filename) do %i.fa.fa-file %strong = wiki_blob.filename .file-content.code.term - = render 'shared/file_hljs', blob: wiki_blob, first_line_number: wiki_blob.startline + = render 'shared/file_highlight', blob: wiki_blob, first_line_number: wiki_blob.startline, user_color_scheme_class: 'white' diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index 5b4816e4c40..60f9e9ac9de 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -1,22 +1,7 @@ -= form_tag search_path, method: :get, class: 'form-horizontal' do |f| - .search-holder.clearfix - .form-group - = label_tag :search, class: 'control-label' do - %span Looking for - .col-sm-6 - = search_field_tag :search, params[:search], placeholder: "issue 143", class: "form-control search-text-input", id: "dashboard_search" - .col-sm-4 - = button_tag 'Search', class: "btn btn-create" - .form-group - .col-sm-2 - - unless params[:snippets].eql? 'true' - .col-sm-10 - = render 'filter', f: f - = hidden_field_tag :project_id, params[:project_id] - = hidden_field_tag :group_id, params[:group_id] - = hidden_field_tag :snippets, params[:snippets] - = hidden_field_tag :scope, params[:scope] - - .results.prepend-top-10 - - if params[:search].present? - = render 'search/results' +- page_title @search_term += render 'search/form' +%hr +- if @search_term + = render 'search/category' + %hr + = render 'search/results' diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml index 299c0bd42a2..000532b1c9a 100644 --- a/app/views/shared/_choose_group_avatar_button.html.haml +++ b/app/views/shared/_choose_group_avatar_button.html.haml @@ -1,4 +1,4 @@ -%a.choose-btn.btn.btn-small.js-choose-group-avatar-button +%a.choose-btn.btn.btn-sm.js-choose-group-avatar-button %i.fa.fa-paperclip %span Choose File ... diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 1cc6043f56b..6de2aed29ed 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -1,10 +1,26 @@ - project = project || @project .git-clone-holder.input-group - .input-group-btn - %button{class: "btn #{ 'active' if default_clone_protocol == 'ssh' }", :"data-clone" => project.ssh_url_to_repo} SSH - %button{class: "btn #{ 'active' if default_clone_protocol == 'http' }", :"data-clone" => project.http_url_to_repo}= gitlab_config.protocol.upcase - = text_field_tag :project_clone, default_url_to_repo(project), class: "one_click_select form-control", readonly: true - - if project.kind_of?(Project) + .input-group-addon.git-protocols + .input-group-btn + %button{ | + type: 'button', | + class: "btn btn-sm #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", | + :"data-clone" => project.ssh_url_to_repo, | + :"data-title" => "Add an SSH key to your profile<br> to pull or push via SSH", + :"data-html" => "true", + :"data-container" => "body"} + SSH + .input-group-btn + %button{ | + type: 'button', | + class: "btn btn-sm #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", | + :"data-clone" => project.http_url_to_repo, | + :"data-title" => "Set a password on your account<br> to pull or push via #{gitlab_config.protocol.upcase}", + :"data-html" => "true", + :"data-container" => "body"} + = gitlab_config.protocol.upcase + = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control input-sm", readonly: true + - if project.kind_of?(Project) && project.empty_repo? .input-group-addon .visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" } = visibility_level_icon(project.visibility_level) diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index 30ba361c860..5f51b0d450f 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -1,4 +1,4 @@ -#modal-confirm-danger.modal.hide{tabindex: -1} +#modal-confirm-danger.modal{tabindex: -1} .modal-dialog .modal-content .modal-header diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index ee0b57fbe5a..334db60690d 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,4 +1,4 @@ -.event_filter +%ul.nav.nav-pills.event_filter = event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.comments, 'Comments' diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml new file mode 100644 index 00000000000..30d37dceb30 --- /dev/null +++ b/app/views/shared/_field.html.haml @@ -0,0 +1,24 @@ +- name = field[:name] +- title = field[:title] || name.humanize +- value = service_field_value(field[:type], @service.send(name)) +- type = field[:type] +- placeholder = field[:placeholder] +- choices = field[:choices] +- default_choice = field[:default_choice] +- help = field[:help] + +.form-group + = form.label name, title, class: "control-label" + .col-sm-10 + - if type == 'text' + = form.text_field name, class: "form-control", placeholder: placeholder + - elsif type == 'textarea' + = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder + - elsif type == 'checkbox' + = form.check_box name + - elsif type == 'select' + = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } + - elsif type == 'password' + = form.password_field name, placeholder: value, class: 'form-control' + - if help + %span.help-block= help diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml new file mode 100644 index 00000000000..d6a2e177da1 --- /dev/null +++ b/app/views/shared/_file_highlight.html.haml @@ -0,0 +1,12 @@ +.file-content.code{class: user_color_scheme_class} + .line-numbers + - if blob.data.present? + - blob.data.lines.each_index do |index| + - offset = defined?(first_line_number) ? first_line_number : 1 + - i = index + offset + -# We're not using `link_to` because it is too slow once we get to thousands of lines. + %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} + %i.fa.fa-link + = i + :preserve + #{highlight(blob.name, blob.data)} diff --git a/app/views/shared/_file_hljs.html.haml b/app/views/shared/_file_hljs.html.haml deleted file mode 100644 index 444c948b026..00000000000 --- a/app/views/shared/_file_hljs.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -%div.highlighted-data{class: user_color_scheme_class} - .line-numbers - - if blob.data.present? - - blob.data.lines.to_a.size.times do |index| - - offset = defined?(first_line_number) ? first_line_number : 1 - - i = index + offset - = link_to "#L#{i}", id: "L#{i}", rel: "#L#{i}" do - %i.fa.fa-link - = i - .highlight - %pre - %code{ class: highlightjs_class(blob.name) } - #{blob.data} diff --git a/app/views/shared/_filter.html.haml b/app/views/shared/_filter.html.haml deleted file mode 100644 index d366dd97a71..00000000000 --- a/app/views/shared/_filter.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -.side-filters - = form_tag filter_path(entity), method: 'get' do - - if current_user - %fieldset.scope-filter - %ul.nav.nav-pills.nav-stacked - %li{class: ("active" if params[:scope] == 'assigned-to-me')} - = link_to filter_path(entity, scope: 'assigned-to-me') do - Assigned to me - %span.pull-right - = assigned_entities_count(current_user, entity, @group) - %li{class: ("active" if params[:scope] == 'authored')} - = link_to filter_path(entity, scope: 'authored') do - Created by me - %span.pull-right - = authored_entities_count(current_user, entity, @group) - %li{class: ("active" if params[:scope] == 'all')} - = link_to filter_path(entity, scope: 'all') do - Everyone's - %span.pull-right - = authorized_entities_count(current_user, entity, @group) - - %fieldset.status-filter - %legend State - %ul.nav.nav-pills - %li{class: ("active" if params[:state] == 'opened')} - = link_to filter_path(entity, state: 'opened') do - Open - %li{class: ("active" if params[:state] == 'closed')} - = link_to filter_path(entity, state: 'closed') do - Closed - %li{class: ("active" if params[:state] == 'all')} - = link_to filter_path(entity, state: 'all') do - All - - %fieldset - %legend Projects - %ul.nav.nav-pills.nav-stacked.nav-small - - @projects.each do |project| - - unless entities_per_project(project, entity).zero? - %li{class: ("active" if params[:project_id] == project.id.to_s)} - = link_to filter_path(entity, project_id: project.id) do - = project.name_with_namespace - %small.pull-right= entities_per_project(project, entity) - - %fieldset - - if params[:state].present? || params[:project_id].present? - = link_to filter_path(entity, state: nil, project_id: nil), class: 'pull-right cgray' do - %i.fa.fa-times - %strong Clear filter - diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index 93294e42505..c0a9923348e 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -1,9 +1,26 @@ +- if @group.persisted? + .form-group + = f.label :name, class: 'control-label' do + Group name + .col-sm-10 + = f.text_field :name, placeholder: 'open-source', class: 'form-control' + .form-group - = f.label :name, class: 'control-label' do - Group name + = f.label :path, class: 'control-label' do + Group path .col-sm-10 - = f.text_field :name, placeholder: 'Example Group', class: 'form-control', - autofocus: local_assigns[:autofocus] || false + .input-group + .input-group-addon + = root_url + = f.text_field :path, placeholder: 'open-source', class: 'form-control', + autofocus: local_assigns[:autofocus] || false + - if @group.persisted? + .alert.alert-warning.prepend-top-10 + %ul + %li Changing group path can have unintended side effects. + %li Renaming group path will rename directory for all related projects + %li It will change web url for access group and group projects. + %li It will change the git path to repositories under this group. .form-group.group-description-holder = f.label :description, 'Details', class: 'control-label' diff --git a/app/views/shared/_issuable_filter.html.haml b/app/views/shared/_issuable_filter.html.haml new file mode 100644 index 00000000000..a5187fa4ea7 --- /dev/null +++ b/app/views/shared/_issuable_filter.html.haml @@ -0,0 +1,69 @@ +.issues-filters + .issues-state-filters + %ul.nav.nav-tabs + %li{class: ("active" if params[:state] == 'opened')} + = link_to page_filter_path(state: 'opened') do + = icon('exclamation-circle') + #{state_filters_text_for(:opened, @project)} + + - if defined?(type) && type == :merge_requests + %li{class: ("active" if params[:state] == 'merged')} + = link_to page_filter_path(state: 'merged') do + = icon('check-circle') + #{state_filters_text_for(:merged, @project)} + + %li{class: ("active" if params[:state] == 'rejected')} + = link_to page_filter_path(state: 'rejected') do + = icon('ban') + #{state_filters_text_for(:rejected, @project)} + - else + %li{class: ("active" if params[:state] == 'closed')} + = link_to page_filter_path(state: 'closed') do + = icon('check-circle') + #{state_filters_text_for(:closed, @project)} + + %li{class: ("active" if params[:state] == 'all')} + = link_to page_filter_path(state: 'all') do + = icon('compass') + #{state_filters_text_for(:all, @project)} + + .issues-details-filters + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name]), method: :get, class: 'filter-form' do + - if controller.controller_name == 'issues' + .check-all-holder + = check_box_tag "check_all_issues", nil, false, + class: "check_all_issues left", + disabled: !can?(current_user, :modify_issue, @project) + .issues-other-filters + .filter-item.inline + = users_select_tag(:assignee_id, selected: params[:assignee_id], + placeholder: 'Assignee', class: 'trigger-submit', any_user: true, null_user: true, first_user: true) + + .filter-item.inline + = users_select_tag(:author_id, selected: params[:author_id], + placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true) + + .filter-item.inline.milestone-filter + = select_tag('milestone_title', projects_milestones_options, class: "select2 trigger-submit", prompt: 'Milestone') + + - if @project + .filter-item.inline.labels-filter + = select_tag('label_name', project_labels_options(@project), class: "select2 trigger-submit", prompt: 'Label') + + .pull-right + = render 'shared/sort_dropdown' + + - if controller.controller_name == 'issues' + .issues_bulk_update.hide + = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do + = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control') + = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true) + = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") + = hidden_field_tag 'update[issues_ids]', [] + = hidden_field_tag :state_event, params[:state_event] + = button_tag "Update issues", class: "btn update_selected_issues btn-save" + +:coffeescript + $('form.filter-form').on 'submit', (event) -> + event.preventDefault() + Turbolinks.visit @.action + '&' + $(@).serialize() diff --git a/app/views/shared/_issuable_search_form.html.haml b/app/views/shared/_issuable_search_form.html.haml new file mode 100644 index 00000000000..58c3de64b77 --- /dev/null +++ b/app/views/shared/_issuable_search_form.html.haml @@ -0,0 +1,9 @@ += form_tag(path, method: :get, id: "issue_search_form", class: 'pull-left issue-search-form') do + .append-right-10.hidden-xs.hidden-sm + = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input' } + = 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/_issues.html.haml b/app/views/shared/_issues.html.haml index e976f897dc9..0dbb6a04393 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -4,7 +4,7 @@ - project = group[0] .panel-heading = link_to_project project - = link_to 'show all', project_issues_path(project), class: 'pull-right' + = link_to 'show all', namespace_project_issues_path(project.namespace, project), class: 'pull-right' %ul.well-list.issues-list - group[1].each do |issue| diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index 39a1ee38f8e..c02c5af008a 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -4,7 +4,7 @@ - project = group[0] .panel-heading = link_to_project project - = link_to 'show all', project_merge_requests_path(project), class: 'pull-right' + = link_to 'show all', namespace_project_merge_requests_path(project.namespace, project), class: 'pull-right' %ul.well-list.mr-list - group[1].each do |merge_request| = render 'projects/merge_requests/merge_request', merge_request: merge_request diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml new file mode 100644 index 00000000000..f685ae7726c --- /dev/null +++ b/app/views/shared/_milestones_filter.html.haml @@ -0,0 +1,14 @@ +.milestones-filters.append-bottom-10 + %ul.nav.nav-tabs + %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')} + = link_to milestones_filter_path(state: 'opened') do + %i.fa.fa-exclamation-circle + Open + %li{class: ("active" if params[:state] == 'closed')} + = link_to milestones_filter_path(state: 'closed') do + %i.fa.fa-check-circle + Closed + %li{class: ("active" if params[:state] == 'all')} + = link_to milestones_filter_path(state: 'all') do + %i.fa.fa-compass + All diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml new file mode 100644 index 00000000000..a43bf33751a --- /dev/null +++ b/app/views/shared/_no_password.html.haml @@ -0,0 +1,8 @@ +- if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.require_password? + .no-password-message.alert.alert-warning.hidden-xs + You won't be able to pull or push project code via #{gitlab_config.protocol.upcase} until you #{link_to 'set a password', edit_profile_password_path} on your account + + .pull-right + = link_to "Don't show again", profile_path(user: {hide_no_password: true}), method: :put + | + = link_to 'Remind later', '#', class: 'hide-no-password-message' diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml index e70eb4d01b9..089179e677a 100644 --- a/app/views/shared/_no_ssh.html.haml +++ b/app/views/shared/_no_ssh.html.haml @@ -1,14 +1,8 @@ -- if cookies[:hide_no_ssh_message].blank? && current_user.require_ssh_key? && !current_user.hide_no_ssh_key - .no-ssh-key-message - .container - You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path} to your profile - .pull-right.hidden-xs - = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'hide-no-ssh-message', remote: true - | - = link_to 'Remind later', '#', class: 'hide-no-ssh-message' - .links-xs.visible-xs - = link_to "Add key", new_profile_key_path - | - = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'hide-no-ssh-message', remote: true - | - = link_to 'Later', '#', class: 'hide-no-ssh-message' +- if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? + .no-ssh-key-message.alert.alert-warning.hidden-xs + You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path, class: 'alert-link'} to your profile + + .pull-right + = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link' + | + = link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link' diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml new file mode 100644 index 00000000000..0eba1fe075f --- /dev/null +++ b/app/views/shared/_outdated_browser.html.haml @@ -0,0 +1,8 @@ +- if outdated_browser? + - link = "https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/requirements.md#supported-web-browsers" + .browser-alert + GitLab may not work properly because you are using an outdated web browser. + %br + Please install a + = link_to 'supported web browser', link + for a better experience. diff --git a/app/views/shared/_project.html.haml b/app/views/shared/_project.html.haml new file mode 100644 index 00000000000..6bd61455d21 --- /dev/null +++ b/app/views/shared/_project.html.haml @@ -0,0 +1,16 @@ += cache [project.namespace, project, controller.controller_name, controller.action_name] do + = link_to project_path(project), class: dom_class(project) do + - if avatar + .dash-project-avatar + = project_icon(project, alt: '', class: 'avatar project-avatar s40') + %span.str-truncated + %span.namespace-name + - if project.namespace + = project.namespace.human_name + \/ + %span.project-name.filter-title + = project.name + - if stars + %span.pull-right.light + %i.fa.fa-star + = project.star_count diff --git a/app/views/shared/_project_filter.html.haml b/app/views/shared/_project_filter.html.haml deleted file mode 100644 index ea6a49e1501..00000000000 --- a/app/views/shared/_project_filter.html.haml +++ /dev/null @@ -1,64 +0,0 @@ -.side-filters - = form_tag project_entities_path, method: 'get' do - - if current_user - %fieldset - %ul.nav.nav-pills.nav-stacked - %li{class: ("active" if params[:scope] == 'all')} - = link_to project_filter_path(scope: 'all') do - Everyone's - %span.pull-right - = authorized_entities_count(current_user, entity, @project) - %li{class: ("active" if params[:scope] == 'assigned-to-me')} - = link_to project_filter_path(scope: 'assigned-to-me') do - Assigned to me - %span.pull-right - = assigned_entities_count(current_user, entity, @project) - %li{class: ("active" if params[:scope] == 'created-by-me')} - = link_to project_filter_path(scope: 'created-by-me') do - Created by me - %span.pull-right - = authored_entities_count(current_user, entity, @project) - - %fieldset - %legend State - %ul.nav.nav-pills - %li{class: ("active" if params[:state] == 'opened')} - = link_to project_filter_path(state: 'opened') do - Open - %li{class: ("active" if params[:state] == 'closed')} - = link_to project_filter_path(state: 'closed') do - Closed - %li{class: ("active" if params[:state] == 'all')} - = link_to project_filter_path(state: 'all') do - All - - - if defined?(labels) - %fieldset - %legend - Labels - %small.pull-right - = link_to project_labels_path(@project), class: 'light' do - %i.fa.fa-pencil-square-o - %ul.nav.nav-pills.nav-stacked.nav-small.labels-filter - - @project.labels.order_by_name.each do |label| - %li{class: label_filter_class(label.name)} - = link_to labels_filter_path(label.name) do - = render_colored_label(label) - - if selected_label?(label.name) - .pull-right - %i.fa.fa-times - - - if @project.labels.empty? - .light-well - Create first label at - = link_to 'labels page', project_labels_path(@project) - %br - or #{link_to 'generate', generate_project_labels_path(@project, redirect: redirect), method: :post} default set of labels - - %fieldset - - if %w(state scope milestone_id assignee_id label_name).select { |k| params[k].present? }.any? - = link_to project_entities_path, class: 'cgray pull-right' do - %i.fa.fa-times - %strong Clear filter - - diff --git a/app/views/shared/_projects_list.html.haml b/app/views/shared/_projects_list.html.haml new file mode 100644 index 00000000000..4c58092af44 --- /dev/null +++ b/app/views/shared/_projects_list.html.haml @@ -0,0 +1,17 @@ +- projects_limit = 20 unless local_assigns[:projects_limit] +- avatar = true unless local_assigns[:avatar] == false +- stars = false unless local_assigns[:stars] == true +%ul.well-list.projects-list + - projects.each_with_index do |project, i| + %li{class: (i >= projects_limit) ? 'project-row hide' : 'project-row'} + = render "shared/project", project: project, avatar: avatar, stars: stars + - if projects.blank? + %li + .nothing-here-block There are no projects here. + - if projects.count > projects_limit + %li.bottom + %span.light + #{projects_limit} of #{pluralize(projects.count, 'project')} displayed. + %span + = link_to '#', class: 'js-expand' do + Show all diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 4d9534f49b1..eb2e1919e19 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,4 +1,4 @@ -= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do += form_tag switch_namespace_project_refs_path(@project.namespace, @project), method: :get, class: "project-refs-form" do = select_tag "ref", grouped_options_refs, class: "project-refs-select select2 select2-sm" = hidden_field_tag :destination, destination - if defined?(path) diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml new file mode 100644 index 00000000000..16a98a7233c --- /dev/null +++ b/app/views/shared/_service_settings.html.haml @@ -0,0 +1,75 @@ +- if @service.errors.any? + #error_explanation + .alert.alert-danger + %ul + - @service.errors.full_messages.each do |msg| + %li= msg + +- if @service.help.present? + .well + = preserve do + = markdown @service.help + +.form-group + = form.label :active, "Active", class: "control-label" + .col-sm-10 + = form.check_box :active + +- if @service.supported_events.length > 1 + .form-group + = form.label :url, "Trigger", class: 'control-label' + .col-sm-10 + - if @service.supported_events.include?("push") + %div + = form.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = form.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + - if @service.supported_events.include?("tag_push") + %div + = form.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = form.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 + - if @service.supported_events.include?("note") + %div + = form.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = form.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This url will be triggered when someone adds a comment + - if @service.supported_events.include?("issue") + %div + = form.check_box :issues_events, class: 'pull-left' + .prepend-left-20 + = form.label :issues_events, class: 'list-label' do + %strong Issues events + %p.light + This url will be triggered when an issue is created + - if @service.supported_events.include?("merge_request") + %div + = form.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = form.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 + +- @service.fields.each do |field| + - type = field[:type] + + - if type == 'fieldset' + - fields = field[:fields] + - legend = field[:legend] + + %fieldset + %legend= legend + - fields.each do |subfield| + = render 'shared/field', form: form, field: subfield + - else + = render 'shared/field', form: form, field: field diff --git a/app/views/shared/_show_aside.html.haml b/app/views/shared/_show_aside.html.haml new file mode 100644 index 00000000000..3ac9b11b4fa --- /dev/null +++ b/app/views/shared/_show_aside.html.haml @@ -0,0 +1,2 @@ += link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 7b37b39780e..af3d35de325 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -1,22 +1,22 @@ .dropdown.inline.prepend-left-10 - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} %span.light sort: - if @sort.present? - = @sort + = sort_options_hash[@sort] - else - Newest + = sort_title_recently_created %b.caret - %ul.dropdown-menu + %ul.dropdown-menu.dropdown-menu-align-right %li - = link_to project_filter_path(sort: 'newest') do - Newest - = link_to project_filter_path(sort: 'oldest') do - Oldest - = link_to project_filter_path(sort: 'recently_updated') do - Recently updated - = link_to project_filter_path(sort: 'last_updated') do - Last updated - = link_to project_filter_path(sort: 'milestone_due_soon') do - Milestone due soon - = link_to project_filter_path(sort: 'milestone_due_later') do - Milestone due later + = 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 + = sort_title_oldest_created + = link_to page_filter_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to page_filter_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated + = link_to page_filter_path(sort: sort_value_milestone_soon) do + = sort_title_milestone_soon + = link_to page_filter_path(sort: sort_value_milestone_later) do + = sort_title_milestone_later diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml new file mode 100644 index 00000000000..1c6ec198d3d --- /dev/null +++ b/app/views/shared/_visibility_level.html.haml @@ -0,0 +1,14 @@ +.form-group.project-visibility-level-holder + = f.label :visibility_level, class: 'control-label' do + Visibility Level + = link_to "(?)", help_page_path("public_access", "public_access") + .col-sm-10 + - if can_change_visibility_level + = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) + - else + .col-sm-10 + %span.info + = visibility_level_icon(visibility_level) + %strong + = visibility_level_label(visibility_level) + .light= visibility_level_description(visibility_level, form_model) diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml new file mode 100644 index 00000000000..02416125a72 --- /dev/null +++ b/app/views/shared/_visibility_radios.html.haml @@ -0,0 +1,14 @@ +- Gitlab::VisibilityLevel.values.each do |level| + .radio + - restricted = restricted_visibility_levels.include?(level) + = form.label "#{model_method}_#{level}" do + = form.radio_button model_method, level, checked: (selected_level == level), disabled: restricted + = visibility_level_icon(level) + .option-title + = visibility_level_label(level) + .option-descr + = visibility_level_description(level, form_model) +- unless restricted_visibility_levels.empty? + .col-sm-10 + %span.info + Some visibility level settings have been restricted by the administrator. diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index 8cec6168ab8..d26a99bb14c 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -1,14 +1,10 @@ - unless @snippet.content.empty? - - if gitlab_markdown?(@snippet.file_name) - .file-content.wiki - = preserve do - = markdown(@snippet.data) - - elsif markup?(@snippet.file_name) + - if markup?(@snippet.file_name) .file-content.wiki = render_markup(@snippet.file_name, @snippet.data) - else .file-content.code - = render 'shared/file_hljs', blob: @snippet + = render 'shared/file_highlight', blob: @snippet - else .file-content.code .nothing-here-block Empty file diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index f729f129e45..913b6744844 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -10,15 +10,15 @@ = f.label :title, class: 'control-label' .col-sm-10= f.text_field :title, placeholder: "Example Snippet", class: 'form-control', required: true - = render "shared/snippets/visibility_level", f: f, visibility_level: gitlab_config.default_projects_features.visibility_level, can_change_visibility_level: true - - .form-group - .file-editor + = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet + + .file-editor + .form-group = f.label :file_name, "File", class: 'control-label' .col-sm-10 .file-holder.snippet .file-title - = f.text_field :file_name, placeholder: "example.rb", class: 'form-control snippet-file-name', required: true + = f.text_field :file_name, placeholder: "Optionally name this file to add code highlighting, e.g. example.rb for Ruby.", class: 'form-control snippet-file-name' .file-content.code %pre#editor= @snippet.content = f.hidden_field :content, class: 'snippet-file-content' @@ -29,8 +29,8 @@ - else = f.submit 'Save', class: "btn-save btn" - - if @snippet.respond_to?(:project) - = link_to "Cancel", project_snippets_path(@project), class: "btn btn-cancel" + - if @snippet.project_id + = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" - else = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" diff --git a/app/views/shared/snippets/_visibility_level.html.haml b/app/views/shared/snippets/_visibility_level.html.haml deleted file mode 100644 index 9acff18e450..00000000000 --- a/app/views/shared/snippets/_visibility_level.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -.form-group.project-visibility-level-holder - = f.label :visibility_level, class: 'control-label' do - Visibility Level - = link_to "(?)", help_page_path("public_access", "public_access") - .col-sm-10 - - if can_change_visibility_level - - Gitlab::VisibilityLevel.values.each do |level| - .radio - - restricted = restricted_visibility_levels.include?(level) - = f.radio_button :visibility_level, level, disabled: restricted - = label "#{dom_class(@snippet)}_visibility_level", level do - = visibility_level_icon(level) - .option-title - = visibility_level_label(level) - .option-descr - = snippet_visibility_level_description(level) - - unless restricted_visibility_levels.empty? - .col-sm-10 - %span.info - Some visibility level settings have been restricted by the administrator. - - else - .col-sm-10 - %span.info - = visibility_level_icon(visibility_level) - %strong - = visibility_level_label(visibility_level) - .light= visibility_level_description(visibility_level) diff --git a/app/views/snippets/_snippet.html.haml b/app/views/snippets/_snippet.html.haml index c584dd8dfb6..5bb28664349 100644 --- a/app/views/snippets/_snippet.html.haml +++ b/app/views/snippets/_snippet.html.haml @@ -11,7 +11,7 @@ %small.pull-right.cgray - if snippet.project_id? - = link_to snippet.project.name_with_namespace, project_path(snippet.project) + = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project) .snippet-info = "##{snippet.id}" diff --git a/app/views/snippets/current_user_index.html.haml b/app/views/snippets/current_user_index.html.haml index b2b7ea4df0e..0718f743828 100644 --- a/app/views/snippets/current_user_index.html.haml +++ b/app/views/snippets/current_user_index.html.haml @@ -1,39 +1,35 @@ +- page_title "Your Snippets" %h3.page-title - My Snippets + Your Snippets .pull-right = link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do Add new snippet - = link_to snippets_path, class: "btn btn-grouped" do - Discover snippets %p.light Share code pastes with others out of git repository -%hr -.row - .col-md-3 - %ul.nav.nav-pills.nav-stacked - = nav_tab :scope, nil do - = link_to user_snippets_path(@user) do - All - %span.pull-right - = @user.snippets.count - = nav_tab :scope, 'are_private' do - = link_to user_snippets_path(@user, scope: 'are_private') do - Private - %span.pull-right - = @user.snippets.are_private.count - = nav_tab :scope, 'are_internal' do - = link_to user_snippets_path(@user, scope: 'are_internal') do - Internal - %span.pull-right - = @user.snippets.are_internal.count - = nav_tab :scope, 'are_public' do - = link_to user_snippets_path(@user, scope: 'are_public') do - Public - %span.pull-right - = @user.snippets.are_public.count +%ul.nav.nav-tabs + = nav_tab :scope, nil do + = link_to user_snippets_path(@user) do + All + %span.badge + = @user.snippets.count + = nav_tab :scope, 'are_private' do + = link_to user_snippets_path(@user, scope: 'are_private') do + Private + %span.badge + = @user.snippets.are_private.count + = nav_tab :scope, 'are_internal' do + = link_to user_snippets_path(@user, scope: 'are_internal') do + Internal + %span.badge + = @user.snippets.are_internal.count + = nav_tab :scope, 'are_public' do + = link_to user_snippets_path(@user, scope: 'are_public') do + Public + %span.badge + = @user.snippets.are_public.count - .col-md-9.my-snippets - = render 'snippets' +.my-snippets + = render 'snippets' diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index 7042d07d5e8..1a380035661 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -1,4 +1,5 @@ +- page_title "Edit", @snippet.title, "Snippets" %h3.page-title Edit snippet %hr -= render "shared/snippets/form", url: snippet_path(@snippet) += render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index 0d71c41e2e7..e9bb6a908d3 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -1,13 +1,13 @@ +- page_title "Public Snippets" %h3.page-title Public snippets .pull-right - - if current_user = link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do Add new snippet = link_to user_snippets_path(current_user), class: "btn btn-grouped" do - My snippets + Your snippets %p.light Public snippets created by you and other users are listed here diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml index 694d7058317..a74d5e792ad 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -1,4 +1,5 @@ +- page_title "New Snippet" %h3.page-title New snippet %hr -= render "shared/snippets/form", url: snippets_path(@snippet) += render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index f5bc543de10..70a95abde6f 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,3 +1,4 @@ +- page_title @snippet.title, "Snippets" %h3.page-title = @snippet.title @@ -17,13 +18,13 @@ %span.light by = link_to user_snippets_path(@snippet.author) do - = image_tag avatar_icon(@snippet.author_email), class: "avatar avatar-inline s16" + = image_tag avatar_icon(@snippet.author_email), class: "avatar avatar-inline s16", alt: '' = @snippet.author_name .back-link - if @snippet.author == current_user = link_to user_snippets_path(current_user) do - ← my snippets + ← your snippets - else = link_to snippets_path do ← discover snippets @@ -31,13 +32,13 @@ .file-holder .file-title %i.fa.fa-file - %span.file_name + %strong = @snippet.file_name - .options + .file-actions .btn-group - if can?(current_user, :modify_personal_snippet, @snippet) - = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-small", title: 'Edit Snippet' - = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-small", target: "_blank" + = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-sm", title: 'Edit Snippet' + = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" - if can?(current_user, :admin_personal_snippet, @snippet) - = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-small btn-remove", title: 'Delete Snippet' + = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet' = render 'shared/snippets/blob' diff --git a/app/views/snippets/user_index.html.haml b/app/views/snippets/user_index.html.haml index 67f3a68aa22..23700eb39da 100644 --- a/app/views/snippets/user_index.html.haml +++ b/app/views/snippets/user_index.html.haml @@ -1,3 +1,4 @@ +- page_title "Snippets", @user.name %h3.page-title = image_tag avatar_icon(@user.email), class: "avatar s24" = @user.name @@ -5,7 +6,7 @@ \/ Snippets - if current_user - = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do + = link_to new_snippet_path, class: "btn btn-sm add_new pull-right", title: "New Snippet" do Add new snippet %hr diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml index ea008c2dede..f360fbb3d5d 100644 --- a/app/views/users/_groups.html.haml +++ b/app/views/users/_groups.html.haml @@ -1,3 +1,4 @@ -- groups.each do |group| - = link_to group, class: 'profile-groups-avatars', :title => group.name do - - image_tag group_icon(group.path) +.clearfix + - groups.each do |group| + = link_to group, class: 'profile-groups-avatars inline', title: group.name do + = image_tag group_icon(group), class: 'avatar group-avatar s40' diff --git a/app/views/users/_profile.html.haml b/app/views/users/_profile.html.haml index 3b44959baad..90d9980c85c 100644 --- a/app/views/users/_profile.html.haml +++ b/app/views/users/_profile.html.haml @@ -5,6 +5,10 @@ %li %span.light Member since %strong= user.created_at.stamp("Aug 21, 2011") + - unless user.public_email.blank? + %li + %span.light E-mail: + %strong= link_to user.public_email, "mailto:#{user.public_email}" - unless user.skype.blank? %li %span.light Skype: @@ -12,7 +16,7 @@ - unless user.linkedin.blank? %li %span.light LinkedIn: - %strong= user.linkedin + %strong= link_to user.linkedin, "http://www.linkedin.com/in/#{user.linkedin}" - unless user.twitter.blank? %li %span.light Twitter: @@ -21,7 +25,7 @@ %li %span.light Website: %strong= link_to user.short_website_url, user.full_website_url - - unless user.bio.blank? + - unless user.location.blank? %li - %span.light Bio: - %span= user.bio + %span.light Location: + %strong= user.location diff --git a/app/views/users/_projects.html.haml b/app/views/users/_projects.html.haml index 1d38f8e8ab8..297fa537394 100644 --- a/app/views/users/_projects.html.haml +++ b/app/views/users/_projects.html.haml @@ -1,6 +1,13 @@ -.panel.panel-default - .panel-heading Personal projects - %ul.well-list - - projects.each do |project| - %li - = link_to_project project +- if local_assigns.has_key?(:contributed_projects) && contributed_projects.present? + .panel.panel-default.contributed-projects + .panel-heading Projects contributed to + = render 'shared/projects_list', + projects: contributed_projects.sort_by(&:star_count).reverse, + projects_limit: 5, stars: true, avatar: false + +- if local_assigns.has_key?(:projects) && projects.present? + .panel.panel-default + .panel-heading Personal projects + = render 'shared/projects_list', + projects: projects.sort_by(&:star_count).reverse, + projects_limit: 10, stars: true, avatar: false diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml new file mode 100644 index 00000000000..922b0c6cebf --- /dev/null +++ b/app/views/users/calendar.html.haml @@ -0,0 +1,12 @@ +%h4 + Contributions calendar + .pull-right + %small Issues, merge requests and push events +#cal-heatmap.calendar + :javascript + new Calendar( + #{@timestamps.to_json}, + #{@starting_year}, + #{@starting_month}, + '#{user_calendar_activities_path}' + ); diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml new file mode 100644 index 00000000000..027a93a75fc --- /dev/null +++ b/app/views/users/calendar_activities.html.haml @@ -0,0 +1,23 @@ +%h4.prepend-top-20 + %span.light 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 + - else + = event.project_name diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder new file mode 100644 index 00000000000..50232dc7186 --- /dev/null +++ b/app/views/users/show.atom.builder @@ -0,0 +1,12 @@ +xml.instruct! +xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do + xml.title "#{@user.name} activity" + xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml" + xml.link href: user_url(@user), rel: "alternate", type: "text/html" + xml.id user_url(@user) + xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + + @events.each do |event| + event_to_atom(xml, event) + end +end diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index cb49c030af2..15d53499e03 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,26 +1,57 @@ +- page_title @user.name +- header_title @user.name, user_path(@user) + += content_for :meta_tags do + = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") + += render 'shared/show_aside' + .row - .col-md-8 - %h3.page-title - = image_tag avatar_icon(@user.email, 90), class: "avatar s90", alt: '' - = @user.name - - if @user == current_user - .pull-right - = link_to profile_path, class: 'btn' do - %i.fa.fa-pencil-square-o - Edit Profile settings - %br - %span.user-show-username #{@user.username} - %br - %small member since #{@user.created_at.stamp("Nov 12, 2031")} + %section.col-md-8 + .header-with-avatar + = link_to avatar_icon(@user.email), target: '_blank' do + = image_tag avatar_icon(@user.email, 90), class: "avatar avatar-tile s90", alt: '' + %h3 + = @user.name + - if @user == current_user + .pull-right.hidden-xs + = link_to profile_path, class: 'btn btn-sm' do + %i.fa.fa-pencil-square-o + Edit Profile settings + .username + @#{@user.username} + .description + - if @user.bio.present? + = @user.bio + .clearfix - if @groups.any? - %h4 Groups: - = render 'groups', groups: @groups + .prepend-top-20 + %h4 Groups + = render 'groups', groups: @groups + %hr + + .hidden-xs + .user-calendar + %h4.center.light + %i.fa.fa-spinner.fa-spin + .user-calendar-activities %hr - %h4 User Activity: - = render @events - .col-md-4 + %h4 + User Activity + + - if current_user + %span.rss-icon.pull-right + = link_to user_path(@user, :atom, { private_token: current_user.private_token }) do + %strong + %i.fa.fa-rss + + .content_list + = spinner + %aside.col-md-4 = render 'profile', user: @user - - if @projects.present? - = render 'projects', projects: @projects + = render 'projects', projects: @projects, contributed_projects: @contributed_projects + +:coffeescript + $(".user-calendar").load("#{user_calendar_path}") diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 788d9065a7b..36ea6742064 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -1,6 +1,10 @@ .votes.votes-block - .progress - .progress-bar.progress-bar-success{style: "width: #{votable.upvotes_in_percent}%;"} - .progress-bar.progress-bar-danger{style: "width: #{votable.downvotes_in_percent}%;"} - .upvotes= "#{votable.upvotes} up" - .downvotes= "#{votable.downvotes} down" + .btn-group + - unless votable.upvotes.zero? + .btn.btn-sm.disabled.cgreen + %i.fa.fa-thumbs-up + = votable.upvotes + - unless votable.downvotes.zero? + .btn.btn-sm.disabled.cred + %i.fa.fa-thumbs-down + = votable.downvotes diff --git a/app/views/votes/_votes_inline.html.haml b/app/views/votes/_votes_inline.html.haml index ee805474830..2cb3ae04e1a 100644 --- a/app/views/votes/_votes_inline.html.haml +++ b/app/views/votes/_votes_inline.html.haml @@ -1,9 +1,9 @@ .votes.votes-inline - unless votable.upvotes.zero? - .upvotes + %span.upvotes.cgreen + #{votable.upvotes} - unless votable.downvotes.zero? \/ - unless votable.downvotes.zero? - .downvotes + %span.downvotes.cred \- #{votable.downvotes} diff --git a/app/workers/auto_merge_worker.rb b/app/workers/auto_merge_worker.rb new file mode 100644 index 00000000000..a6dd73eee5f --- /dev/null +++ b/app/workers/auto_merge_worker.rb @@ -0,0 +1,13 @@ +class AutoMergeWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform(merge_request_id, current_user_id, params) + params = params.with_indifferent_access + current_user = User.find(current_user_id) + merge_request = MergeRequest.find(merge_request_id) + merge_request.should_remove_source_branch = params[:should_remove_source_branch] + merge_request.automerge!(current_user, params[:commit_message]) + end +end diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index e3f6f3a6aef..1d21addece6 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -1,25 +1,58 @@ class EmailsOnPushWorker include Sidekiq::Worker - def perform(project_id, recipients, push_data) + def perform(project_id, recipients, push_data, options = {}) + options.symbolize_keys! + options.reverse_merge!( + send_from_committer_email: false, + disable_diffs: false + ) + send_from_committer_email = options[:send_from_committer_email] + disable_diffs = options[:disable_diffs] + project = Project.find(project_id) before_sha = push_data["before"] after_sha = push_data["after"] - branch = push_data["ref"] + ref = push_data["ref"] author_id = push_data["user_id"] - if before_sha =~ /^000000/ || after_sha =~ /^000000/ - # skip if new branch was pushed or branch was removed - return true - end + action = + if Gitlab::Git.blank_ref?(before_sha) + :create + elsif Gitlab::Git.blank_ref?(after_sha) + :delete + else + :push + end + + compare = nil + reverse_compare = false + if action == :push + compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) - compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) + return false if compare.same - # Do not send emails if git compare failed - return false unless compare && compare.commits.present? + if compare.commits.empty? + compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) + + reverse_compare = true + + return false if compare.commits.empty? + end + end recipients.split(" ").each do |recipient| - Notify.repository_push_email(project_id, recipient, author_id, branch, compare).deliver + Notify.repository_push_email( + project_id, + 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 end ensure compare = nil diff --git a/app/workers/fork_registration_worker.rb b/app/workers/fork_registration_worker.rb new file mode 100644 index 00000000000..fffa8b3a659 --- /dev/null +++ b/app/workers/fork_registration_worker.rb @@ -0,0 +1,12 @@ +class ForkRegistrationWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform(from_project_id, to_project_id, private_token) + from_project = Project.find(from_project_id) + to_project = Project.find(to_project_id) + + from_project.gitlab_ci_service.fork_registration(to_project, private_token) + end +end diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb new file mode 100644 index 00000000000..84a54656df2 --- /dev/null +++ b/app/workers/irker_worker.rb @@ -0,0 +1,168 @@ +require 'json' +require 'socket' + +class IrkerWorker + include Sidekiq::Worker + + def perform(project_id, chans, colors, push_data, settings) + project = Project.find(project_id) + + # Get config parameters + return false unless init_perform settings, chans, colors + + repo_name = push_data['repository']['name'] + committer = push_data['user_name'] + branch = push_data['ref'].gsub(%r'refs/[^/]*/', '') + + if @colors + repo_name = "\x0304#{repo_name}\x0f" + branch = "\x0305#{branch}\x0f" + end + + # Firsts messages are for branch creation/deletion + send_branch_updates push_data, project, repo_name, committer, branch + + # Next messages are for commits + send_commits push_data, project, repo_name, committer, branch + + close_connection + true + end + + private + + def init_perform(set, chans, colors) + @colors = colors + @channels = chans + start_connection set['server_ip'], set['server_port'] + end + + def start_connection(irker_server, irker_port) + begin + @socket = TCPSocket.new irker_server, irker_port + rescue Errno::ECONNREFUSED => e + logger.fatal "Can't connect to Irker daemon: #{e}" + return false + end + true + end + + def sendtoirker(privmsg) + to_send = { to: @channels, privmsg: privmsg } + @socket.puts JSON.dump(to_send) + end + + def close_connection + @socket.close + end + + def send_branch_updates(push_data, project, repo_name, committer, branch) + if Gitlab::Git.blank_ref?(push_data['before']) + send_new_branch project, repo_name, committer, branch + elsif Gitlab::Git.blank_ref?(push_data['after']) + send_del_branch repo_name, committer, branch + end + end + + def send_new_branch(project, repo_name, committer, branch) + repo_path = project.path_with_namespace + newbranch = "#{Gitlab.config.gitlab.url}/#{repo_path}/branches" + newbranch = "\x0302\x1f#{newbranch}\x0f" if @colors + + privmsg = "[#{repo_name}] #{committer} has created a new branch " + privmsg += "#{branch}: #{newbranch}" + sendtoirker privmsg + end + + def send_del_branch(repo_name, committer, branch) + privmsg = "[#{repo_name}] #{committer} has deleted the branch #{branch}" + sendtoirker privmsg + end + + def send_commits(push_data, project, repo_name, committer, branch) + return if push_data['total_commits_count'] == 0 + + # Next message is for number of commit pushed, if any + if Gitlab::Git.blank_ref?(push_data['before']) + # Tweak on push_data["before"] in order to have a nice compare URL + push_data['before'] = before_on_new_branch push_data, project + end + + send_commits_count(push_data, project, repo_name, committer, branch) + + # One message per commit, limited by 3 messages (same limit as the + # github irc hook) + commits = push_data['commits'].first(3) + commits.each do |hook_attrs| + send_one_commit project, hook_attrs, repo_name, branch + end + end + + def before_on_new_branch(push_data, project) + commit = commit_from_id project, push_data['commits'][0]['id'] + parents = commit.parents + # Return old value if there's no new one + return push_data['before'] if parents.empty? + # Or return the first parent-commit + parents[0].id + end + + def send_commits_count(data, project, repo, committer, branch) + url = compare_url data, project.path_with_namespace + commits = colorize_commits data['total_commits_count'] + + new_commits = 'new commit' + new_commits += 's' if data['total_commits_count'] > 1 + + sendtoirker "[#{repo}] #{committer} pushed #{commits} #{new_commits} " \ + "to #{branch}: #{url}" + end + + def compare_url(data, repo_path) + sha1 = Commit::truncate_sha(data['before']) + sha2 = Commit::truncate_sha(data['after']) + compare_url = "#{Gitlab.config.gitlab.url}/#{repo_path}/compare" + compare_url += "/#{sha1}...#{sha2}" + colorize_url compare_url + end + + def send_one_commit(project, hook_attrs, repo_name, branch) + commit = commit_from_id project, hook_attrs['id'] + sha = colorize_sha Commit::truncate_sha(hook_attrs['id']) + author = hook_attrs['author']['name'] + files = colorize_nb_files(files_count commit) + title = commit.title + + sendtoirker "#{repo_name}/#{branch} #{sha} #{author} (#{files}): #{title}" + end + + def commit_from_id(project, id) + project.commit(id) + end + + def files_count(commit) + files = "#{commit.diffs.count} file" + files += 's' if commit.diffs.count > 1 + files + end + + def colorize_sha(sha) + sha = "\x0314#{sha}\x0f" if @colors + sha + end + + def colorize_nb_files(nb_files) + nb_files = "\x0312#{nb_files}\x0f" if @colors + nb_files + end + + def colorize_url(url) + url = "\x0302\x1f#{url}\x0f" if @colors + url + end + + def colorize_commits(commits) + commits = "\x02#{commits}\x0f" if @colors + commits + end +end diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 1406cba2db3..33d8cc8861b 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -11,8 +11,8 @@ class PostReceive log("Check gitlab.yml config for correct gitlab_shell.repos_path variable. \"#{Gitlab.config.gitlab_shell.repos_path}\" does not match \"#{repo_path}\"") end - repo_path.gsub!(/\.git$/, "") - repo_path.gsub!(/^\//, "") + repo_path.gsub!(/\.git\z/, "") + repo_path.gsub!(/\A\//, "") project = Project.find_with_namespace(repo_path) @@ -21,7 +21,9 @@ class PostReceive return false end - changes = changes.lines if changes.kind_of?(String) + changes = Base64.decode64(changes) unless changes.include?(" ") + changes = utf8_encode_changes(changes) + changes = changes.lines changes.each do |change| oldrev, newrev, ref = change.strip.split(' ') @@ -33,7 +35,7 @@ class PostReceive return false end - if tag?(ref) + if Gitlab::Git.tag_ref?(ref) GitTagPushService.new.execute(project, @user, oldrev, newrev, ref) else GitPushService.new.execute(project, @user, oldrev, newrev, ref) @@ -41,13 +43,20 @@ class PostReceive end end - def log(message) - Gitlab::GitLogger.error("POST-RECEIVE: #{message}") - end + def utf8_encode_changes(changes) + changes = changes.dup + + changes.force_encoding("UTF-8") + return changes if changes.valid_encoding? - private + # Convert non-UTF-8 branch/tag names to UTF-8 so they can be dumped as JSON. + detection = CharlockHolmes::EncodingDetector.detect(changes) + return changes unless detection && detection[:encoding] - def tag?(ref) - !!(/refs\/tags\/(.*)/.match(ref)) + CharlockHolmes::Converter.convert(changes, detection[:encoding], 'UTF-8') + end + + def log(message) + Gitlab::GitLogger.error("POST-RECEIVE: #{message}") end end diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb new file mode 100644 index 00000000000..64d39c4d3f7 --- /dev/null +++ b/app/workers/project_service_worker.rb @@ -0,0 +1,10 @@ +class ProjectServiceWorker + include Sidekiq::Worker + + sidekiq_options queue: :project_web_hook + + def perform(hook_id, data) + data = data.with_indifferent_access + Service.find(hook_id).execute(data) + end +end diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/project_web_hook_worker.rb index 9f9b9b1df5f..fb878965288 100644 --- a/app/workers/project_web_hook_worker.rb +++ b/app/workers/project_web_hook_worker.rb @@ -3,7 +3,8 @@ class ProjectWebHookWorker sidekiq_options queue: :project_web_hook - def perform(hook_id, data) - WebHook.find(hook_id).execute data + def perform(hook_id, data, hook_name) + data = data.with_indifferent_access + WebHook.find(hook_id).execute(data, hook_name) end end diff --git a/app/workers/repository_archive_worker.rb b/app/workers/repository_archive_worker.rb new file mode 100644 index 00000000000..021c1139568 --- /dev/null +++ b/app/workers/repository_archive_worker.rb @@ -0,0 +1,43 @@ +class RepositoryArchiveWorker + include Sidekiq::Worker + + sidekiq_options queue: :archive_repo + + attr_accessor :project, :ref, :format + + def perform(project_id, ref, format) + @project = Project.find(project_id) + @ref, @format = ref, format.downcase + + repository = project.repository + + repository.clean_old_archives + + return unless file_path + return if archived? || archiving? + + repository.archive_repo(ref, storage_path, format) + end + + private + + def storage_path + Gitlab.config.gitlab.repository_downloads_path + end + + def file_path + @file_path ||= project.repository.archive_file_path(ref, storage_path, format) + end + + def pid_file_path + @pid_file_path ||= project.repository.archive_pid_file_path(ref, storage_path, format) + end + + def archived? + File.exist?(file_path) + end + + def archiving? + File.exist?(pid_file_path) + end +end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 01586150cd2..e6a50afedb1 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -6,17 +6,29 @@ class RepositoryImportWorker def perform(project_id) project = Project.find(project_id) - result = gitlab_shell.send(:import_repository, + + import_result = gitlab_shell.send(:import_repository, project.path_with_namespace, project.import_url) + return project.import_fail unless import_result + + data_import_result = if project.import_type == 'github' + Gitlab::GithubImport::Importer.new(project).execute + elsif project.import_type == 'gitlab' + Gitlab::GitlabImport::Importer.new(project).execute + elsif project.import_type == 'bitbucket' + Gitlab::BitbucketImport::Importer.new(project).execute + elsif project.import_type == 'google_code' + Gitlab::GoogleCodeImport::Importer.new(project).execute + else + true + end + return project.import_fail unless data_import_result - if result - project.import_finish - project.save - project.satellite.create unless project.satellite.exists? - project.update_repository_size - else - project.import_fail - end + project.import_finish + project.save + project.satellite.create unless project.satellite.exists? + project.update_repository_size + Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket' end end diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb index 3ebc62b7e7a..a122c274763 100644 --- a/app/workers/system_hook_worker.rb +++ b/app/workers/system_hook_worker.rb @@ -3,7 +3,7 @@ class SystemHookWorker sidekiq_options queue: :system_hook - def perform(hook_id, data) - SystemHook.find(hook_id).execute data + def perform(hook_id, data, hook_name) + SystemHook.find(hook_id).execute(data, hook_name) end end diff --git a/bin/background_jobs b/bin/background_jobs index 59a51c5c868..a041a4b0433 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -37,7 +37,7 @@ start_no_deamonize() start_sidekiq() { - bundle exec sidekiq -q post_receive -q mailer -q system_hook -q project_web_hook -q gitlab_shell -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 + bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 } load_ok() diff --git a/bin/pkgr_before_precompile.sh b/bin/pkgr_before_precompile.sh index 283abb6a0cd..5a2007f4ab0 100755 --- a/bin/pkgr_before_precompile.sh +++ b/bin/pkgr_before_precompile.sh @@ -18,6 +18,3 @@ rm config/resque.yml # Set default unicorn.rb file echo "" > config/unicorn.rb - -# Required for assets precompilation -sudo service postgresql start @@ -3,6 +3,5 @@ begin load File.expand_path("../spring", __FILE__) rescue LoadError end -require_relative '../config/boot' -require 'rake' -Rake.application.run +require 'bundler/setup' +load Gem.bin_path('rake', 'rake') diff --git a/bin/rspec b/bin/rspec index 41e37089ac2..20060ebd79c 100755 --- a/bin/rspec +++ b/bin/rspec @@ -4,4 +4,4 @@ begin rescue LoadError end require 'bundler/setup' -load Gem.bin_path('rspec', 'rspec') +load Gem.bin_path('rspec-core', 'rspec') diff --git a/bin/spring b/bin/spring index 253ec37c345..7b45d374fcd 100755 --- a/bin/spring +++ b/bin/spring @@ -1,17 +1,14 @@ #!/usr/bin/env ruby -# This file loads spring without using Bundler, in order to be fast -# It gets overwritten when you run the `spring binstub` command +# This file loads spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. unless defined?(Spring) require "rubygems" require "bundler" - if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m) - ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) - ENV["GEM_HOME"] = "" - Gem.paths = ENV - + if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) + Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq } gem "spring", match[1] require "spring/binstub" end diff --git a/config.ru b/config.ru index e90863a5c21..a2525c81361 100644 --- a/config.ru +++ b/config.ru @@ -2,11 +2,14 @@ if defined?(Unicorn) require 'unicorn' - # Unicorn self-process killer - require 'unicorn/worker_killer' - # Max memory size (RSS) per worker - use Unicorn::WorkerKiller::Oom, (200 * (1 << 20)), (250 * (1 << 20)) + if ENV['RAILS_ENV'] == 'production' || ENV['RAILS_ENV'] == 'staging' + # Unicorn self-process killer + require 'unicorn/worker_killer' + + # Max memory size (RSS) per worker + use Unicorn::WorkerKiller::Oom, (200 * (1 << 20)), (250 * (1 << 20)) + end end require ::File.expand_path('../config/environment', __FILE__) diff --git a/config/application.rb b/config/application.rb index 8a280de6fac..7e899cc3b5b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -12,11 +12,11 @@ module Gitlab # -- all .rb files in that directory are automatically loaded. # Custom directories with classes and modules you want to be autoloadable. - config.autoload_paths += %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) + 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)) # 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. @@ -31,7 +31,7 @@ module Gitlab config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters += [:password] + config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt) # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true @@ -50,6 +50,8 @@ module Gitlab # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' + config.action_view.sanitized_allowed_protocols = %w(smb) + # Relative url support # Uncomment and customize the last line to run in a non-root path # WARNING: We recommend creating a FQDN to host GitLab in a root path instead of this. @@ -70,7 +72,10 @@ module Gitlab config.middleware.use Rack::Cors do allow do origins '*' - resource '/api/*', headers: :any, methods: [:get, :post, :options, :put, :delete] + resource '/api/*', + headers: :any, + methods: [:get, :post, :options, :put, :delete], + expose: ['Link'] end end diff --git a/config/aws.yml.example b/config/aws.yml.example index 29d029b078d..bb10c3cec7b 100644 --- a/config/aws.yml.example +++ b/config/aws.yml.example @@ -1,5 +1,8 @@ # See https://github.com/jnicklas/carrierwave#using-amazon-s3 # for more options +# If you change this file in a Merge Request, please also create +# a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests +# production: access_key_id: AKIA1111111111111UA secret_access_key: secret diff --git a/config/environments/production.rb b/config/environments/production.rb index 78bf543402b..3316ece3873 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -11,8 +11,9 @@ Gitlab::Application.configure do # Disable Rails's static asset server (Apache or nginx will already do this) config.serve_static_assets = false - # Compress JavaScripts and CSS - config.assets.compress = true + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass # Don't fallback to assets pipeline if a precompiled asset is missed config.assets.compile = true @@ -74,7 +75,6 @@ Gitlab::Application.configure do config.action_mailer.raise_delivery_errors = true config.eager_load = true - config.assets.js_compressor = :uglifier config.allow_concurrency = false end diff --git a/config/environments/test.rb b/config/environments/test.rb index 25b082b98da..2d5e7addcd3 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -5,7 +5,7 @@ Gitlab::Application.configure do # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true + config.cache_classes = false # Configure static asset server for tests with Cache-Control for performance config.serve_static_assets = true diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 7b4c180fccc..c32ac2042d0 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -2,11 +2,23 @@ # GitLab application config file # # # # # # # # # # # # # # # # # # # # +########################### NOTE ##################################### +# This file should not receive new settings. All configuration options # +# that do not require application restart are being moved to # +# ApplicationSetting model! # +# If you change this file in a Merge Request, please also create # +# a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests # +######################################################################## +# +# # How to use: # 1. Copy file as gitlab.yml # 2. Update gitlab -> host with your fully qualified domain name # 3. Update gitlab -> email_from # 4. If you installed Git from source, change git -> bin_path to /usr/local/bin/git +# IMPORTANT: If Git was installed in a different location use that instead. +# You can check with `which git`. If a wrong path of Git is specified, it will +# result in various issues such as failures of GitLab CI builds. # 5. Review this configuration file for other settings you may want to adjust production: &base @@ -43,41 +55,28 @@ production: &base # email_enabled: true # Email address used in the "From" field in mails sent by GitLab email_from: example@example.com + email_display_name: GitLab + email_reply_to: noreply@example.com - # Email server smtp settings are in [a separate file](initializers/smtp_settings.rb.sample). + # Email server smtp settings are in config/initializers/smtp_settings.rb.sample - ## User settings - default_projects_limit: 10 # default_can_create_group: false # default: true # username_changing_enabled: false # default: true - User can change her username/namespace - ## Default theme - ## BASIC = 1 - ## MARS = 2 - ## MODERN = 3 - ## GRAY = 4 - ## COLOR = 5 + ## Default theme ID + ## 1 - Graphite + ## 2 - Charcoal + ## 3 - Green + ## 4 - Gray + ## 5 - Violet + ## 6 - Blue # default_theme: 2 # default: 2 - ## Users can create accounts - # This also allows normal users to sign up for accounts themselves - # default: false - By default GitLab administrators must create all new accounts - # signup_enabled: true - - ## Standard login settings - # The standard login can be disabled to force login via LDAP - # default: true - If set to false the standard login form won't be shown on the sign-in page - # signin_enabled: false - - # Restrict setting visibility levels for non-admin users. - # The default is to allow all levels. - # restricted_visibility_levels: [ "public" ] - ## Automatic issue closing # If a commit message matches this regular expression, all issues referenced from the matched text will be closed. # This happens when the commit is pushed or merged into the default branch of a project. # When not specified the default issue_closing_pattern as specified below will be used. - # Tip: you can test your closing pattern at http://rubular.com - # issue_closing_pattern: '([Cc]lose[sd]|[Ff]ixe[sd]) #(\d+)' + # Tip: you can test your closing pattern at http://rubular.com. + # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' ## Default project features settings default_projects_features: @@ -85,7 +84,6 @@ production: &base merge_requests: true wiki: true snippets: false - visibility_level: "private" # can be "private" | "internal" | "public" ## Webhook settings # Number of seconds to wait for HTTP response after sending webhook HTTP POST request (default: 10) @@ -96,35 +94,6 @@ production: &base # The default is 'tmp/repositories' relative to the root of the Rails app. # repository_downloads_path: tmp/repositories - ## External issues trackers - issues_tracker: - # redmine: - # title: "Redmine" - # ## If not nil, link 'Issues' on project page will be replaced with this - # ## Use placeholders: - # ## :project_id - GitLab project identifier - # ## :issues_tracker_id - Project Name or Id in external issue tracker - # project_url: "http://redmine.sample/projects/:issues_tracker_id" - # - # ## If not nil, links from /#\d/ entities from commit messages will replaced with this - # ## Use placeholders: - # ## :project_id - GitLab project identifier - # ## :issues_tracker_id - Project Name or Id in external issue tracker - # ## :id - Issue id (from commit messages) - # issues_url: "http://redmine.sample/issues/:id" - # - # ## If not nil, links to creating new issues will be replaced with this - # ## Use placeholders: - # ## :project_id - GitLab project identifier - # ## :issues_tracker_id - Project Name or Id in external issue tracker - # new_issue_url: "http://redmine.sample/projects/:issues_tracker_id/issues/new" - # - # jira: - # title: "Atlassian Jira" - # project_url: "http://jira.sample/issues/?jql=project=:issues_tracker_id" - # issues_url: "http://jira.sample/browse/:id" - # new_issue_url: "http://jira.sample/secure/CreateIssue.jspa" - ## Gravatar ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html gravatar: @@ -143,6 +112,15 @@ production: &base ldap: enabled: false servers: + ########################################################################## + # + # Since GitLab 7.4, LDAP servers get ID's (below the ID is 'main'). GitLab + # Enterprise Edition now supports connecting to multiple LDAP servers. + # + # If you are updating from the old (pre-7.4) syntax, you MUST give your + # old server the ID 'main'. + # + ########################################################################## main: # 'main' is the GitLab 'provider ID' of this LDAP server ## label # @@ -153,9 +131,9 @@ production: &base label: 'LDAP' host: '_your_ldap_server' - port: 636 + port: 389 uid: 'sAMAccountName' - method: 'ssl' # "tls" or "ssl" or "plain" + method: 'plain' # "tls" or "ssl" or "plain" bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' password: '_the_password_of_the_bind_user' @@ -175,6 +153,11 @@ production: &base # disable this setting, because the userPrincipalName contains an '@'. allow_username_or_email_login: false + # To maintain tight control over the number of active users on your GitLab installation, + # enable this setting to keep new users blocked until they have been cleared by the admin + # (default: false). + block_auto_created_users: false + # Base where we can search for users # # Ex. ou=People,dc=gitlab,dc=example @@ -204,12 +187,19 @@ production: &base # Allow login via Twitter, Google, etc. using OmniAuth providers enabled: false + # Uncomment this to automatically sign in with a specific omniauth provider's without + # showing GitLab's sign-in page (default: show the GitLab sign-in page) + # auto_sign_in_with_provider: saml + # CAUTION! # This allows users to login without having a user account first (default: false). # User accounts will be created automatically when authentication was successful. allow_single_sign_on: false # Locks down those users until they have been cleared by the admin (default: true). block_auto_created_users: true + # Look up new users in LDAP servers. If a match is found (same uid), automatically + # link the omniauth identity with the LDAP account. (default: false) + auto_link_ldap_user: false ## Auth providers # Uncomment the following lines and fill in the data of the auth provider you want to use @@ -219,14 +209,28 @@ production: &base # arguments, followed by optional 'args' which can be either a hash or an array. # Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html providers: - # - { name: 'google_oauth2', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET', + # - { name: 'google_oauth2', app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET', # args: { access_type: 'offline', approval_prompt: '' } } - # - { name: 'twitter', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET'} - # - { name: 'github', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET', + # - { name: 'twitter', app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET'} + # - { name: 'github', app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET', # args: { scope: 'user:email' } } + # - { name: 'gitlab', app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET', + # args: { scope: 'api' } } + # - { name: 'bitbucket', app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET'} + # - { name: 'saml', + # args: { + # assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + # idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + # idp_sso_target_url: 'https://login.example.com/idp', + # issuer: 'https://gitlab.example.com', + # name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + # } } + @@ -253,6 +257,9 @@ production: &base # aws_secret_access_key: 'secret123' # # The remote 'directory' to store your backups. For S3, this would be the bucket name. # remote_directory: 'my.s3.bucket' + # # Use multipart uploads when file size reaches 100MB, see + # # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html + # multipart_chunk_size: 104857600 ## GitLab Shell settings gitlab_shell: @@ -262,6 +269,10 @@ production: &base repos_path: /home/git/repositories/ hooks_path: /home/git/gitlab-shell/hooks/ + # File that contains the secret key for verifying access for gitlab-shell. + # Default is '.gitlab_shell_secret' relative to Rails.root (i.e. root of the GitLab app). + # secret_file: /home/git/gitlab/.gitlab_shell_secret + # Git over HTTP upload_pack: true receive_pack: true @@ -293,10 +304,22 @@ production: &base # piwik_url: '_your_piwik_url' # piwik_site_id: '_your_piwik_site_id' - ## Text under sign-in page (Markdown enabled) - # sign_in_text: | - #  - # [Learn more about CompanyName](http://www.companydomain.com/) + rack_attack: + git_basic_auth: + # Rack Attack IP banning enabled + # enabled: true + # + # Whitelist requests from 127.0.0.1 for web proxies (NGINX/Apache) with incorrect headers + # ip_whitelist: ["127.0.0.1"] + # + # Limit the number of Git HTTP authentication attempts per IP + # maxretry: 10 + # + # Reset the auth attempt counter per IP after 60 seconds + # findtime: 60 + # + # Ban an IP for one hour (3600s) after too many auth attempts + # bantime: 3600 development: <<: *base diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 27bb83784ba..7b5d488f59e 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -1,3 +1,5 @@ +require 'gitlab' # Load lib/gitlab.rb as soon as possible + class Settings < Settingslogic source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" } namespace Rails.env @@ -13,7 +15,11 @@ class Settings < Settingslogic if gitlab_shell.ssh_port != 22 "ssh://#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}:#{gitlab_shell.ssh_port}/" else - "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}:" + if gitlab_shell.ssh_host.include? ':' + "[#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}]:" + else + "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}:" + end end end @@ -60,15 +66,17 @@ Settings.ldap['enabled'] = false if Settings.ldap['enabled'].nil? # backwards compatibility, we only have one host if Settings.ldap['enabled'] || Rails.env.test? if Settings.ldap['host'].present? + # We detected old LDAP configuration syntax. Update the config to make it + # look like it was entered with the new syntax. server = Settings.ldap.except('sync_time') - server['provider_name'] = 'ldap' Settings.ldap['servers'] = { - 'ldap' => server + 'main' => server } end Settings.ldap['servers'].each do |key, server| server['label'] ||= 'LDAP' + server['block_auto_created_users'] = false if server['block_auto_created_users'].nil? server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil? server['active_directory'] = true if server['active_directory'].nil? server['provider_name'] ||= "ldap#{key}".downcase @@ -76,8 +84,14 @@ if Settings.ldap['enabled'] || Rails.env.test? end end + Settings['omniauth'] ||= Settingslogic.new({}) 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['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil? +Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? + Settings.omniauth['providers'] ||= [] Settings['issues_tracker'] ||= {} @@ -87,8 +101,9 @@ Settings['issues_tracker'] ||= {} # Settings['gitlab'] ||= Settingslogic.new({}) Settings.gitlab['default_projects_limit'] ||= 10 +Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? -Settings.gitlab['default_theme'] = Gitlab::Theme::MARS if Settings.gitlab['default_theme'].nil? +Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil? Settings.gitlab['host'] ||= 'localhost' Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? @@ -97,6 +112,8 @@ Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? Settings.gitlab['email_from'] ||= "gitlab@#{Settings.gitlab.host}" +Settings.gitlab['email_display_name'] ||= "GitLab" +Settings.gitlab['email_reply_to'] ||= "noreply@#{Settings.gitlab.host}" Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url) Settings.gitlab['user'] ||= 'git' Settings.gitlab['user_home'] ||= begin @@ -105,19 +122,23 @@ rescue ArgumentError # no user configured '/home/' + Settings.gitlab['user'] end Settings.gitlab['time_zone'] ||= nil -Settings.gitlab['signup_enabled'] ||= false +Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil? Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? +Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? -Settings.gitlab['issue_closing_pattern'] = '([Cc]lose[sd]|[Ff]ixe[sd]) #(\d+)' if Settings.gitlab['issue_closing_pattern'].nil? +Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil? 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['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.absolute_path(Settings.gitlab['repository_downloads_path'] || 'tmp/repositories', Rails.root) +Settings.gitlab['restricted_signup_domains'] ||= [] # # Gravatar @@ -133,6 +154,7 @@ Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}? Settings['gitlab_shell'] ||= Settingslogic.new({}) Settings.gitlab_shell['path'] ||= Settings.gitlab['user_home'] + '/gitlab-shell/' Settings.gitlab_shell['hooks_path'] ||= Settings.gitlab['user_home'] + '/gitlab-shell/hooks/' +Settings.gitlab_shell['secret_file'] ||= Rails.root.join('.gitlab_shell_secret') Settings.gitlab_shell['receive_pack'] = true if Settings.gitlab_shell['receive_pack'].nil? Settings.gitlab_shell['upload_pack'] = true if Settings.gitlab_shell['upload_pack'].nil? Settings.gitlab_shell['repos_path'] ||= Settings.gitlab['user_home'] + '/repositories/' @@ -148,11 +170,12 @@ Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.send(:build_gitlab_shell_s Settings['backup'] ||= Settingslogic.new({}) Settings.backup['keep_time'] ||= 0 Settings.backup['path'] = File.expand_path(Settings.backup['path'] || "tmp/backups/", Rails.root) -Settings.backup['upload'] ||= Settingslogic.new({'remote_directory' => nil, 'connection' => nil}) +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'] Settings.backup['upload']['connection'] = Hash[Settings.backup['upload']['connection'].map { |k, v| [k.to_sym, v] }] end +Settings.backup['upload']['multipart_chunk_size'] ||= 104857600 # # Git @@ -172,6 +195,17 @@ Settings.satellites['timeout'] ||= 30 Settings['extra'] ||= Settingslogic.new({}) # +# Rack::Attack settings +# +Settings['rack_attack'] ||= Settingslogic.new({}) +Settings.rack_attack['git_basic_auth'] ||= Settingslogic.new({}) +Settings.rack_attack.git_basic_auth['enabled'] = true if Settings.rack_attack.git_basic_auth['enabled'].nil? +Settings.rack_attack.git_basic_auth['ip_whitelist'] ||= %w{127.0.0.1} +Settings.rack_attack.git_basic_auth['maxretry'] ||= 10 +Settings.rack_attack.git_basic_auth['findtime'] ||= 1.minute +Settings.rack_attack.git_basic_auth['bantime'] ||= 1.hour + +# # Testing settings # if Rails.env.test? diff --git a/config/initializers/2_app.rb b/config/initializers/2_app.rb index 655590dff0b..688cdf5f4b0 100644 --- a/config/initializers/2_app.rb +++ b/config/initializers/2_app.rb @@ -6,8 +6,3 @@ module Gitlab Settings end end - -# -# Load all libs for threadsafety -# -Dir["#{Rails.root}/lib/**/*.rb"].each { |file| require file } diff --git a/config/initializers/4_sidekiq.rb b/config/initializers/4_sidekiq.rb index 75c543c0f47..e856499732e 100644 --- a/config/initializers/4_sidekiq.rb +++ b/config/initializers/4_sidekiq.rb @@ -15,7 +15,7 @@ Sidekiq.configure_server do |config| config.server_middleware do |chain| chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] - chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MAX_RSS'] + chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] end end diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb index 7c2e7f39000..80d641d73a3 100644 --- a/config/initializers/5_backend.rb +++ b/config/initializers/5_backend.rb @@ -6,3 +6,10 @@ require Rails.root.join("lib", "gitlab", "backend", "shell") # GitLab shell adapter require 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) + +unless current_version.valid? && required_version <= current_version + warn "WARNING: This version of GitLab depends on gitlab-shell #{required_version}, but you're running #{current_version}. Please update gitlab-shell." +end diff --git a/config/initializers/6_rack_profiler.rb b/config/initializers/6_rack_profiler.rb index a7ee3c59822..5312fd8e89a 100644 --- a/config/initializers/6_rack_profiler.rb +++ b/config/initializers/6_rack_profiler.rb @@ -1,6 +1,10 @@ -if Rails.env == 'development' +if Rails.env.development? require 'rack-mini-profiler' # initialization is skipped so trigger it Rack::MiniProfilerRails.initialize!(Rails.application) + + Rack::MiniProfiler.config.position = 'right' + Rack::MiniProfiler.config.start_hidden = true + Rack::MiniProfiler.config.skip_paths << '/teaspoon' end diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb index 18759f0cfb0..df73ec1304a 100644 --- a/config/initializers/7_omniauth.rb +++ b/config/initializers/7_omniauth.rb @@ -9,4 +9,19 @@ if Gitlab::LDAP::Config.enabled? server = Gitlab.config.ldap.servers.values.first alias_method server['provider_name'], :ldap end -end
\ No newline at end of file +end + +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) +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.new(env).call +end + +if Gitlab.config.omniauth.enabled + Gitlab.config.omniauth.providers.each do |provider| + if provider['name'] == 'kerberos' + require 'omniauth-kerberos' + end + end +end diff --git a/config/initializers/8_default_url_options.rb b/config/initializers/8_default_url_options.rb new file mode 100644 index 00000000000..8fd27b1d88e --- /dev/null +++ b/config/initializers/8_default_url_options.rb @@ -0,0 +1,11 @@ +default_url_options = { + host: Gitlab.config.gitlab.host, + protocol: Gitlab.config.gitlab.protocol, + script_name: Gitlab.config.gitlab.relative_url_root +} + +unless Gitlab.config.gitlab_on_standard_port? + default_url_options[:port] = Gitlab.config.gitlab.port +end + +Rails.application.routes.default_url_options = default_url_options diff --git a/config/initializers/acts_as_taggable_on_patch.rb b/config/initializers/acts_as_taggable_on_patch.rb deleted file mode 100644 index baa77fde392..00000000000 --- a/config/initializers/acts_as_taggable_on_patch.rb +++ /dev/null @@ -1,130 +0,0 @@ -# This is a patch to address the issue in https://github.com/mbleigh/acts-as-taggable-on/issues/427 caused by -# https://github.com/rails/rails/commit/31a43ebc107fbd50e7e62567e5208a05909ec76c -# gem 'acts-as-taggable-on' has the fix included https://github.com/mbleigh/acts-as-taggable-on/commit/89bbed3864a9252276fb8dd7d535fce280454b90 -# but not in the currently used version of gem ('2.4.1') -# With replacement of 'acts-as-taggable-on' gem this file will become obsolete - -module ActsAsTaggableOn::Taggable - module Core - module ClassMethods - def tagged_with(tags, options = {}) - tag_list = ActsAsTaggableOn::TagList.from(tags) - empty_result = where("1 = 0") - - return empty_result if tag_list.empty? - - joins = [] - conditions = [] - having = [] - select_clause = [] - - context = options.delete(:on) - owned_by = options.delete(:owned_by) - alias_base_name = undecorated_table_name.gsub('.','_') - quote = ActsAsTaggableOn::Tag.using_postgresql? ? '"' : '' - - if options.delete(:exclude) - if options.delete(:wild) - tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ") - else - tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ") - end - - conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)})" - - if owned_by - joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" + - " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" + - " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{owned_by.id}" + - " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}" - end - - elsif options.delete(:any) - # get tags, drop out if nothing returned (we need at least one) - tags = if options.delete(:wild) - ActsAsTaggableOn::Tag.named_like_any(tag_list) - else - ActsAsTaggableOn::Tag.named_any(tag_list) - end - - return empty_result unless tags.length > 0 - - # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123 - # avoid ambiguous column name - taggings_context = context ? "_#{context}" : '' - - taggings_alias = adjust_taggings_alias( - "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}" - ) - - tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" + - " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" - tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context - - # don't need to sanitize sql, map all ids and join with OR logic - conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ") - select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one? - - if owned_by - tagging_join << " AND " + - sanitize_sql([ - "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?", - owned_by.id, - owned_by.class.base_class.to_s - ]) - end - - joins << tagging_join - else - tags = ActsAsTaggableOn::Tag.named_any(tag_list) - - return empty_result unless tags.length == tag_list.length - - tags.each do |tag| - taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}") - tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" + - " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" + - " AND #{taggings_alias}.tag_id = #{tag.id}" - - tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context - - if owned_by - tagging_join << " AND " + - sanitize_sql([ - "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?", - owned_by.id, - owned_by.class.base_class.to_s - ]) - end - - joins << tagging_join - end - end - - taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group" - - if options.delete(:match_all) - joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" + - " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" - - - group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}" - group = group_columns - having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}" - end - - select(select_clause) \ - .joins(joins.join(" ")) \ - .where(conditions.join(" AND ")) \ - .group(group) \ - .having(having) \ - .order(options[:order]) \ - .readonly(false) - end - end - end -end diff --git a/config/initializers/attr_encrypted_no_db_connection.rb b/config/initializers/attr_encrypted_no_db_connection.rb new file mode 100644 index 00000000000..c668864089b --- /dev/null +++ b/config/initializers/attr_encrypted_no_db_connection.rb @@ -0,0 +1,20 @@ +module AttrEncrypted + module Adapters + module ActiveRecord + def attribute_instance_methods_as_symbols_with_no_db_connection + # Use with_connection so the connection doesn't stay pinned to the thread. + connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false + + if connected + # Call version from AttrEncrypted::Adapters::ActiveRecord + attribute_instance_methods_as_symbols_without_no_db_connection + else + # Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord + AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call + end + end + + alias_method_chain :attribute_instance_methods_as_symbols, :no_db_connection + end + end +end diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index d0065b63e54..bfb8656df55 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -12,22 +12,30 @@ if File.exists?(aws_file) aws_secret_access_key: AWS_CONFIG['secret_access_key'], # required region: AWS_CONFIG['region'], # optional, defaults to 'us-east-1' } - config.fog_directory = AWS_CONFIG['bucket'] # required - config.fog_public = false # optional, defaults to true - config.fog_attributes = {'Cache-Control'=>'max-age=315576000'} # optional, defaults to {} - config.fog_authenticated_url_expiration = 1 << 29 # optional time (in seconds) that authenticated urls will be valid. - # when fog_public is false and provider is AWS or Google, defaults to 600 + + # required + config.fog_directory = AWS_CONFIG['bucket'] + + # optional, defaults to true + config.fog_public = false + + # optional, defaults to {} + 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 + config.fog_authenticated_url_expiration = 1 << 29 end # Mocking Fog requests, based on: https://github.com/carrierwaveuploader/carrierwave/wiki/How-to%3A-Test-Fog-based-uploaders if Rails.env.test? Fog.mock! connection = ::Fog::Storage.new( - :aws_access_key_id => AWS_CONFIG['access_key_id'], - :aws_secret_access_key => AWS_CONFIG['secret_access_key'], - :provider => 'AWS', - :region => AWS_CONFIG['region'] + aws_access_key_id: AWS_CONFIG['access_key_id'], + aws_secret_access_key: AWS_CONFIG['secret_access_key'], + provider: 'AWS', + region: AWS_CONFIG['region'] ) - connection.directories.create(:key => AWS_CONFIG['bucket']) + connection.directories.create(key: AWS_CONFIG['bucket']) end end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index c6eb3e51036..091548348b1 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,14 +1,14 @@ # Use this hook to configure devise mailer, warden hooks and so forth. The first # four configuration values can also be set straight in your models. Devise.setup do |config| - # ==> Mailer Configuration - # Configure the e-mail address which will be shown in Devise::Mailer, - # note that it will be overwritten if you use your own mailer class with default "from" parameter. - config.mailer_sender = "GitLab <#{Gitlab.config.gitlab.email_from}>" - + config.warden do |manager| + manager.default_strategies(scope: :user).unshift :two_factor_authenticatable + manager.default_strategies(scope: :user).unshift :two_factor_backupable + end + # ==> Mailer Configuration # Configure the class responsible to send e-mails. - # config.mailer = "Devise::Mailer" + config.mailer = "DeviseMailer" # ==> ORM configuration # Load and configure the ORM. Supports :active_record (default) and @@ -145,7 +145,8 @@ Devise.setup do |config| # Time interval you can reset your password with a reset password key. # Don't put a too small interval or your users won't have the time to # change their passwords. - config.reset_password_within = 2.hours + # When someone else invites you to GitLab this time is also used so it should be pretty long. + config.reset_password_within = 2.days # ==> Configuration for :encryptable # Allow you to use another encryption algorithm besides bcrypt (default). You can use @@ -207,7 +208,7 @@ Devise.setup do |config| if Gitlab::LDAP::Config.enabled? Gitlab.config.ldap.servers.values.each do |server| if server['allow_username_or_email_login'] - email_stripping_proc = ->(name) {name.gsub(/@.*$/,'')} + email_stripping_proc = ->(name) {name.gsub(/@.*\z/,'')} else email_stripping_proc = ->(name) {name} end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 00000000000..d422acb31d6 --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,102 @@ +Doorkeeper.configure do + # Change the ORM that doorkeeper will use. + # Currently supported options are :active_record, :mongoid2, :mongoid3, :mongo_mapper + orm :active_record + + # This block will be called to check whether the resource owner is authenticated or not. + resource_owner_authenticator do + # Put your resource owner authentication logic here. + # Example implementation: + current_user || redirect_to(new_user_session_url) + end + + resource_owner_from_credentials do |routes| + u = User.find_by(email: params[:username]) || User.find_by(username: params[:username]) + u if u && u.valid_password?(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. + # admin_authenticator do + # # Put your admin authentication logic here. + # # Example implementation: + # Admin.find_by_id(session[:admin_id]) || redirect_to(new_admin_session_url) + # end + + # Authorization Code expiration time (default 10 minutes). + # authorization_code_expires_in 10.minutes + + # Access token expiration time (default 2 hours). + # If you want to disable expiration, set this to nil. + access_token_expires_in nil + + # Reuse access token for the same resource owner within an application (disabled by default) + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 + # reuse_access_token + + # Issue access tokens with refresh token (disabled by default) + use_refresh_token + + # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled + # by default in non-development environments). OAuth2 delegates security in + # communication to the HTTPS protocol so it is wise to keep this enabled. + # + force_ssl_in_redirect_uri false + + # Provide support for an owner to be assigned to each registered application (disabled by default) + # Optional parameter confirmation: true (default false) if you want to enforce ownership of + # a registered application + # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support + enable_application_owner confirmation: false + + # Define access token scopes for your provider + # For more information go to + # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes + default_scopes :api + #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 + # falls back to the `:client_id` and `:client_secret` params from the `params` object. + # Check out the wiki for more information on customization + # client_credentials :from_basic, :from_params + + # Change the way access token is authenticated from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:access_token` or `:bearer_token` params from the `params` object. + # Check out the wiki for more information on customization + access_token_methods :from_access_token_param, :from_bearer_authorization, :from_bearer_param + + # Change the native redirect uri for client apps + # When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider + # 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' + + # Specify what grant flows are enabled in array of Strings. The valid + # strings and the flows they enable are: + # + # "authorization_code" => Authorization Code Grant Flow + # "implicit" => Implicit Grant Flow + # "password" => Resource Owner Password Credentials Grant Flow + # "client_credentials" => Client Credentials Grant Flow + # + # If not specified, Doorkeeper enables all the four grant flows. + # + grant_flows %w(authorization_code password client_credentials) + + # Under some circumstances you might want to have applications auto-approved, + # so that the user skips the authorization step. + # For example if dealing with trusted a application. + # skip_authorization do |resource_owner, client| + # client.superapp? or resource_owner.admin? + # end + + # WWW-Authenticate Realm (default "Doorkeeper"). + # realm "Doorkeeper" + + # Allow dynamic query parameters (disabled by default) + # Some applications require dynamic query parameters on their request_uri + # set to true if you want this to be allowed + # wildcard_redirect_uri false +end diff --git a/config/initializers/gitlab_shell_secret_token.rb b/config/initializers/gitlab_shell_secret_token.rb index 8d2b771e535..751fccead07 100644 --- a/config/initializers/gitlab_shell_secret_token.rb +++ b/config/initializers/gitlab_shell_secret_token.rb @@ -5,8 +5,7 @@ require 'securerandom' # Your secret key for verifying the gitlab_shell. -secret_file = Rails.root.join('.gitlab_shell_secret') -gitlab_shell_symlink = File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret') +secret_file = Gitlab.config.gitlab_shell.secret_file unless File.exist? secret_file # Generate a new token of 16 random hexadecimal characters and store it in secret_file. @@ -14,6 +13,7 @@ unless File.exist? secret_file File.write(secret_file, token) end -if File.exist?(Gitlab.config.gitlab_shell.path) && !File.exist?(gitlab_shell_symlink) - FileUtils.symlink(secret_file, gitlab_shell_symlink) -end
\ No newline at end of file +link_path = File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret') +if File.exist?(Gitlab.config.gitlab_shell.path) && !File.exist?(link_path) + FileUtils.symlink(secret_file, link_path) +end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 8f8bef42bef..ca58ae92d1b 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -6,3 +6,5 @@ Mime::Type.register_alias "text/plain", :diff Mime::Type.register_alias "text/plain", :patch +Mime::Type.register_alias 'text/html', :markdown +Mime::Type.register_alias 'text/html', :md diff --git a/config/initializers/public_key.rb b/config/initializers/public_key.rb new file mode 100644 index 00000000000..e4f09a2d020 --- /dev/null +++ b/config/initializers/public_key.rb @@ -0,0 +1,2 @@ +path = File.expand_path("~/.ssh/bitbucket_rsa.pub") +Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path) diff --git a/config/initializers/rack_attack.rb.example b/config/initializers/rack_attack.rb.example index 332865d2881..b1bbcca1d61 100644 --- a/config/initializers/rack_attack.rb.example +++ b/config/initializers/rack_attack.rb.example @@ -1,6 +1,7 @@ # 1. Rename this file to rack_attack.rb # 2. Review the paths_to_be_protected and add any other path you need protecting # +# If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests paths_to_be_protected = [ "#{Rails.application.config.relative_url_root}/users/password", diff --git a/config/initializers/rack_attack_git_basic_auth.rb b/config/initializers/rack_attack_git_basic_auth.rb new file mode 100644 index 00000000000..bbbfed68329 --- /dev/null +++ b/config/initializers/rack_attack_git_basic_auth.rb @@ -0,0 +1,12 @@ +unless Rails.env.test? + # Tell the Rack::Attack Rack middleware to maintain an IP blacklist. We will + # update the blacklist from Grack::Auth#authenticate_user. + Rack::Attack.blacklist('Git HTTP Basic Auth') do |req| + Rack::Attack::Allow2Ban.filter(req.ip, Gitlab.config.rack_attack.git_basic_auth) do + # This block only gets run if the IP was not already banned. + # Return false, meaning that we do not see anything wrong with the + # request at this time + false + end + end +end diff --git a/config/initializers/redis-store-fix-expiry.rb b/config/initializers/redis-store-fix-expiry.rb new file mode 100644 index 00000000000..fce0a135330 --- /dev/null +++ b/config/initializers/redis-store-fix-expiry.rb @@ -0,0 +1,44 @@ +# Monkey-patch Redis::Store to make 'setex' and 'expire' work with namespacing + +module Gitlab + class Redis + class Store + module Namespace + # Redis::Store#setex in redis-store 1.1.4 does not respect namespaces; + # this new method does. + def setex(key, expires_in, value, options=nil) + namespace(key) { |key| super(key, expires_in, value) } + end + + # Redis::Store#expire in redis-store 1.1.4 does not respect namespaces; + # this new method does. + def expire(key, expires_in) + namespace(key) { |key| super(key, expires_in) } + end + + private + + # Our new definitions of #setex and #expire above assume that the + # #namespace method exists. Because we cannot be sure of that, we + # re-implement the #namespace method from Redis::Store::Namespace so + # that it is available for all Redis::Store instances, whether they use + # namespacing or not. + # + # Based on lib/redis/store/namespace.rb L49-51 (redis-store 1.1.4) + def namespace(key) + if @namespace + yield interpolate(key) + else + # This Redis::Store instance does not use a namespace so we should + # just pass through the key. + yield key + end + end + end + end + end +end + +Redis::Store.class_eval do + include Gitlab::Redis::Store::Namespace +end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index b2d59f1c4b7..6d274cd95a1 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,11 +1,15 @@ # Be sure to restart your server when you modify this file. +require 'gitlab/current_settings' +include Gitlab::CurrentSettings +Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay + Gitlab::Application.config.session_store( :redis_store, # Using the cookie_store would enable session replay attacks. servers: Gitlab::Application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store key: '_gitlab_session', secure: Gitlab.config.gitlab.https, httponly: true, - expire_after: 1.week, + expire_after: Settings.gitlab['session_expire_delay'] * 60, path: (Rails.application.config.relative_url_root.nil?) ? '/' : Rails.application.config.relative_url_root ) diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample index 3711b03796e..25ec247a095 100644 --- a/config/initializers/smtp_settings.rb.sample +++ b/config/initializers/smtp_settings.rb.sample @@ -1,8 +1,12 @@ -# To enable smtp email delivery for your GitLab instance do next: +# To enable smtp email delivery for your GitLab instance do the following: # 1. Rename this file to smtp_settings.rb # 2. Edit settings inside this file # 3. Restart GitLab instance # +# For full list of options and their values see http://api.rubyonrails.org/classes/ActionMailer/Base.html +# +# If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests + if Rails.env.production? Gitlab::Application.config.action_mailer.delivery_method = :smtp @@ -13,6 +17,7 @@ if Rails.env.production? password: "123456", domain: "gitlab.company.com", authentication: :login, - enable_starttls_auto: true + enable_starttls_auto: true, + openssl_verify_mode: 'peer' # See ActionMailer documentation for other possible options } end diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb new file mode 100644 index 00000000000..d9042c652bb --- /dev/null +++ b/config/initializers/static_files.rb @@ -0,0 +1,15 @@ +app = Rails.application + +if app.config.serve_static_assets + # The `ActionDispatch::Static` middleware intercepts requests for static files + # by checking if they exist in the `/public` directory. + # We're replacing it with our `Gitlab::Middleware::Static` that does the same, + # except ignoring `/uploads`, letting those go through to the GitLab Rails app. + + app.config.middleware.swap( + ActionDispatch::Static, + Gitlab::Middleware::Static, + app.paths["public"].first, + app.config.static_cache_control + ) +end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 1cbcde5b3da..f3db5b7476e 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -23,8 +23,8 @@ en: timeout: 'Your session expired, please sign in again to continue.' inactive: 'Your account was not activated yet.' sessions: - signed_in: 'Signed in successfully.' - signed_out: 'Signed out successfully.' + signed_in: '' + signed_out: '' users_sessions: user: signed_in: 'Signed in successfully.' @@ -57,4 +57,4 @@ en: reset_password_instructions: subject: 'Reset password instructions' unlock_instructions: - subject: 'Unlock Instructions' + subject: 'Unlock Instructions'
\ No newline at end of file diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml new file mode 100644 index 00000000000..a4032a21420 --- /dev/null +++ b/config/locales/doorkeeper.en.yml @@ -0,0 +1,73 @@ +en: + activerecord: + errors: + models: + application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + relative_uri: 'must be an absolute URI.' + mongoid: + errors: + models: + application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + relative_uri: 'must be an absolute URI.' + mongo_mapper: + errors: + models: + application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + relative_uri: 'must be an absolute URI.' + doorkeeper: + errors: + messages: + # Common error messages + invalid_request: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' + invalid_redirect_uri: 'The redirect URI included is not valid.' + unauthorized_client: 'The client is not authorized to perform this request using this method.' + access_denied: 'The resource owner or authorization server denied the request.' + invalid_scope: 'The requested scope is invalid, unknown, or malformed.' + server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.' + temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.' + + #configuration error messages + credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.' + resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.' + + # Access grant errors + unsupported_response_type: 'The authorization server does not support this response type.' + + # Access token errors + invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.' + invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' + unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.' + + # Password Access token errors + invalid_resource_owner: 'The provided resource owner credentials are not valid, or resource owner cannot be found' + + invalid_token: + revoked: "The access token was revoked" + expired: "The access token expired" + unknown: "The access token is invalid" + scopes: + api: Access your API + + flash: + applications: + create: + notice: 'The application was created successfully.' + destroy: + notice: 'The application was deleted successfully.' + update: + notice: 'The application was updated successfully.' + authorized_applications: + destroy: + notice: 'The application was revoked access.' diff --git a/config/resque.yml.example b/config/resque.yml.example index 347f3599b20..d98f43f71b2 100644 --- a/config/resque.yml.example +++ b/config/resque.yml.example @@ -1,3 +1,6 @@ +# If you change this file in a Merge Request, please also create +# a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests +# development: redis://localhost:6379 test: redis://localhost:6379 production: unix:/var/run/redis/redis.sock diff --git a/config/routes.rb b/config/routes.rb index 723104daf13..d60bc796fdb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,11 +2,20 @@ require 'sidekiq/web' require 'api/api' Gitlab::Application.routes.draw do - # + use_doorkeeper do + controllers applications: 'oauth/applications', + authorized_applications: 'oauth/authorized_applications', + authorizations: 'oauth/authorizations' + end + + # Autocomplete + get '/autocomplete/users' => 'autocomplete#users' + get '/autocomplete/users/:id' => 'autocomplete#user' + + # Search - # - get 'search' => "search#show" - get 'search/autocomplete' => "search#autocomplete", as: :search_autocomplete + get 'search' => 'search#show' + get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete # API API::API.logger Rails.logger @@ -15,9 +24,9 @@ Gitlab::Application.routes.draw do # Get all keys of user get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ } - constraint = lambda { |request| request.env["warden"].authenticate? and request.env['warden'].user.admin? } + constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? } constraints constraint do - mount Sidekiq::Web, at: "/admin/sidekiq", as: :sidekiq + mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq end # Enable Grack support @@ -28,26 +37,95 @@ Gitlab::Application.routes.draw do receive_pack: Gitlab.config.gitlab_shell.receive_pack }), at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post] - # # Help - # - get 'help' => 'help#index' - get 'help/:category/:file' => 'help#show', as: :help_page + get 'help/:category/:file' => 'help#show', as: :help_page, constraints: { category: /.*/, file: /[^\/\.]+/ } get 'help/shortcuts' + get 'help/ui' => 'help#ui' # # Global snippets # resources :snippets do member do - get "raw" + get 'raw' + end + end + + get '/s/:username' => 'snippets#index', as: :user_snippets, constraints: { username: /.*/ } + + # + # Invites + # + + resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do + member do + post :accept + match :decline, via: [:get, :post] + end + end + + # + # Import + # + namespace :import do + resource :github, only: [:create, :new], controller: :github do + get :status + get :callback + get :jobs + end + + resource :gitlab, only: [:create, :new], controller: :gitlab do + get :status + get :callback + get :jobs + end + + resource :bitbucket, only: [:create, :new], controller: :bitbucket do + get :status + get :callback + get :jobs + end + + resource :gitorious, only: [:create, :new], controller: :gitorious do + get :status + get :callback + get :jobs + end + + resource :google_code, only: [:create, :new], controller: :google_code do + get :status + post :callback + get :jobs + + get :new_user_map, path: :user_map + post :create_user_map, path: :user_map end end - get "/s/:username" => "snippets#user_index", as: :user_snippets, constraints: { username: /.*/ } # - # Explroe area + # Uploads + # + + scope path: :uploads do + # Note attachments and User/Group/Project avatars + get ":model/:mounted_as/:id/:filename", + to: "uploads#show", + constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ } + + # Project markdown uploads + get ":namespace_id/:project_id/:secret/:filename", + to: "projects/uploads#show", + constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ } + end + + # Redirect old note attachments path to new uploads path. + get "files/note/:id/:filename", + to: redirect("uploads/note/attachment/%{id}/%{filename}"), + constraints: { filename: /[^\/]+/ } + + # + # Explore area # namespace :explore do resources :projects, only: [:index] do @@ -58,23 +136,19 @@ Gitlab::Application.routes.draw do end resources :groups, only: [:index] - root to: "projects#trending" + root to: 'projects#trending' end # Compatibility with old routing - get 'public' => "explore/projects#index" - get 'public/projects' => "explore/projects#index" - - # - # Attachments serving - # - get 'files/:type/:id/:filename' => 'files#download', constraints: { id: /\d+/, type: /[a-z]+/, filename: /.+/ } + get 'public' => 'explore/projects#index' + get 'public/projects' => 'explore/projects#index' # # Admin Area # namespace :admin do resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do + resources :keys, only: [:show, :destroy] member do put :team_update put :block @@ -83,12 +157,16 @@ Gitlab::Application.routes.draw do end end + resources :applications + resources :groups, constraints: { id: /[^\/]+/ } do member do - put :project_teams_update + put :members_update end end + resources :deploy_keys, only: [:index, :new, :create, :destroy] + resources :hooks, only: [:index, :create, :destroy] do get :test end @@ -97,13 +175,26 @@ Gitlab::Application.routes.draw do resource :logs, only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] - resources :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }, only: [:index, :show] do - member do - put :transfer + resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do + root to: 'projects#index', as: :projects + + resources(:projects, + path: '/', + constraints: { id: /[a-zA-Z.0-9_\-]+/ }, + only: [:index, :show]) do + root to: 'projects#show' + + member do + put :transfer + end end end - root to: "dashboard#index" + resource :application_settings, only: [:show, :update] do + resources :services + end + + root to: 'dashboard#index' end # @@ -112,59 +203,85 @@ Gitlab::Application.routes.draw do resource :profile, only: [:show, :update] do member do get :history - get :design + get :applications put :reset_private_token put :update_username end scope module: :profiles do - resource :account, only: [:show, :update] + resource :account, only: [:show, :update] do + member do + delete :unlink + end + end resource :notifications, only: [:show, :update] resource :password, only: [:new, :create, :edit, :update] do member do put :reset end end + resource :preferences, only: [:show, :update] resources :keys resources :emails, only: [:index, :create, :destroy] - resources :groups, only: [:index] do + resource :avatar, only: [:destroy] + resource :two_factor_auth, only: [:new, :create, :destroy] do member do - delete :leave + post :codes end end - resource :avatar, only: [:destroy] end end - match "/u/:username" => "users#show", as: :user, constraints: { username: /.*/ }, via: :get + 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' => 'users#show', as: :user, + constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } # # Dashboard Area # - resource :dashboard, controller: "dashboard", only: [:show] do + resource :dashboard, controller: 'dashboard', only: [:show] do member do - get :projects get :issues get :merge_requests end + + scope module: :dashboard do + resources :milestones, only: [:index, :show] + + resources :groups, only: [:index] + + resources :projects, only: [] do + collection do + get :starred + end + end + end end # # Groups Area # - resources :groups, constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/} do + resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do member do get :issues get :merge_requests - get :members get :projects end scope module: :groups do - resources :group_members, only: [:create, :update, :destroy] + resources :group_members, only: [:index, :create, :update, :destroy] do + post :resend_invite, on: :member + delete :leave, on: :collection + end + resource :avatar, only: [:destroy] - resources :milestones + resources :milestones, only: [:index, :show, :update] end end @@ -173,170 +290,241 @@ Gitlab::Application.routes.draw do 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 + get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error end + + root to: "root#show" + # # Project Area # - resources :projects, constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/ }, except: [:new, :create, :index], path: "/" do - member do - put :transfer - post :archive - post :unarchive - post :upload_image - post :toggle_star - get :autocomplete_sources - end - - scope module: :projects do - resources :blob, only: [:show, :destroy], constraints: { id: /.+/ } do - get :diff, on: :member - end - resources :raw, only: [:show], constraints: {id: /.+/} - resources :tree, only: [:show], constraints: {id: /.+/, format: /(html|js)/ } - resources :edit_tree, only: [:show, :update], constraints: { id: /.+/ }, path: 'edit' do - post :preview, on: :member + 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 + post :archive + post :unarchive + post :toggle_star + post :markdown_preview + get :autocomplete_sources end - resources :new_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'new' - resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/} - resources :commits, only: [:show], constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/} - resources :compare, only: [:index, :create] - resources :blame, only: [:show], constraints: {id: /.+/} - resources :network, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/} - resources :graphs, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/} do - member do - get :commits + + scope module: :projects do + # Blob routes: + get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob' + post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob' + get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob' + put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob' + post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob' + + scope do + get( + '/blob/*id/diff', + to: 'blob#diff', + constraints: { id: /.+/, format: false }, + as: :blob_diff + ) + get( + '/blob/*id', + to: 'blob#show', + constraints: { id: /.+/, format: false }, + as: :blob + ) + delete( + '/blob/*id', + to: 'blob#destroy', + constraints: { id: /.+/, format: false } + ) end - end - match "/compare/:from...:to" => "compare#show", as: "compare", via: [:get, :post], constraints: {from: /.+/, to: /.+/} + scope do + get( + '/raw/*id', + to: 'raw#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :raw + ) + end - resources :snippets, constraints: {id: /\d+/} do - member do - get "raw" + scope do + get( + '/tree/*id', + to: 'tree#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :tree + ) end - end - resources :wikis, only: [:show, :edit, :destroy, :create], constraints: {id: /[a-zA-Z.0-9_\-\/]+/} do - collection do - get :pages - put ':id' => 'wikis#update' - get :git_access + scope do + get( + '/blame/*id', + to: 'blame#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :blame + ) end - member do - get "history" + scope do + get( + '/commits/*id', + to: 'commits#show', + constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }, + as: :commits + ) end - end - resource :fork, only: [:new, :create] - resource :import, only: [:new, :create, :show] + resource :avatar, only: [:show, :destroy] + resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do + get :branches, on: :member + end - resource :repository, only: [:show, :create] do - member do - get "archive", constraints: { format: Gitlab::Regex.archive_formats_regex } + resources :compare, only: [:index, :create] + resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } + + resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do + member do + get :commits + end end - end - resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do - member do - get :test + get '/compare/:from...:to' => 'compare#show', :as => 'compare', + :constraints => { from: /.+/, to: /.+/ } + + resources :snippets, constraints: { id: /\d+/ } do + member do + get 'raw' + end end - end - resources :deploy_keys, constraints: {id: /\d+/} do - member do - put :enable - put :disable + resources :wikis, only: [:show, :edit, :destroy, :create], constraints: { id: /[a-zA-Z.0-9_\-\/]+/ } do + collection do + get :pages + put ':id' => 'wikis#update' + get :git_access + end + + member do + get 'history' + end + end + + resource :repository, only: [:show, :create] do + member do + get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex } + end end - end - resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resources :protected_branches, only: [:index, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do + member do + get :test + end + end - resources :refs, only: [] do - collection do - get "switch" + resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do + member do + put :enable + put :disable + end end - member do - # tree viewer logs - get "logs_tree", constraints: { id: Gitlab::Regex.git_reference_regex } - get "logs_tree/:path" => "refs#logs_tree", - as: :logs_file, - constraints: { - id: Gitlab::Regex.git_reference_regex, + resource :fork, only: [:new, :create] + resource :import, only: [:new, :create, :show] + + resources :refs, only: [] do + collection do + get 'switch' + end + + member do + # tree viewer logs + get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex } + get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: { + id: Gitlab::Regex.git_reference_regex, path: /.*/ } + end end - end - resources :merge_requests, constraints: {id: /\d+/}, except: [:destroy] do - member do - get :diffs - post :automerge - get :automerge_check - get :ci_status + resources :merge_requests, constraints: { id: /\d+/ }, except: [:destroy] do + member do + get :diffs + get :commits + post :automerge + get :automerge_check + get :ci_status + post :toggle_subscription + end + + collection do + get :branch_from + get :branch_to + get :update_branches + end end - collection do - get :branch_from - get :branch_to - get :update_branches - end - end + resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resources :hooks, only: [:index, :create, :destroy], constraints: {id: /\d+/} do - member do - get :test + resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do + member do + get :test + end end - end - resources :team, controller: 'team_members', only: [:index] - resources :milestones, except: [:destroy], constraints: {id: /\d+/} do - member do - put :sort_issues - put :sort_merge_requests + resources :milestones, except: [:destroy], constraints: { id: /\d+/ } do + member do + put :sort_issues + put :sort_merge_requests + end end - end - resources :labels, constraints: {id: /\d+/} do - collection do - post :generate + resources :labels, constraints: { id: /\d+/ } do + collection do + post :generate + end end - end - resources :issues, constraints: {id: /\d+/}, except: [:destroy] do - collection do - post :bulk_update + resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do + member do + post :toggle_subscription + end + collection do + post :bulk_update + end end - end - resources :team_members, except: [:index, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do - collection do - delete :leave + resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do + collection do + delete :leave + + # Used for import team + # from another project + get :import + post :apply_import + end - # Used for import team - # from another project - get :import - post :apply_import + member do + post :resend_invite + end end - end - resources :notes, only: [:index, :create, :destroy, :update], constraints: {id: /\d+/} do - member do - delete :delete_attachment + resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do + member do + delete :delete_attachment + end end - collection do - post :preview + resources :uploads, only: [:create] do + collection do + get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ } + end end end + end end - get ':id' => "namespaces#show", constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/} - - root to: "dashboard#show" + get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } end diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index ea22744fd90..b937b092789 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -8,15 +8,18 @@ # See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete # documentation. +# Note: If you change this file in a Merge Request, please also create a +# Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests +# # WARNING: See config/application.rb under "Relative url support" for the list of # other files that need to be changed for relative url support # # ENV['RAILS_RELATIVE_URL_ROOT'] = "/gitlab" -# Use at least one worker per core if you're on a dedicated server, -# more will usually help for _short_ waits on databases/caches. -# The minimum is 2 -worker_processes 2 +# Read about unicorn workers here: +# http://doc.gitlab.com/ee/install/requirements.html#unicorn-workers +# +worker_processes 3 # Since Unicorn is never exposed to outside clients, it does not need to # run on the standard HTTP port (80), there is no reason to start Unicorn @@ -37,10 +40,10 @@ listen "127.0.0.1:8080", :tcp_nopush => true # nuke workers after 30 seconds instead of 60 seconds (the default) # -# NOTICE: git push over http depends on this value. -# If you want be able to push huge amount of data to git repository over http -# you will have to increase this value too. -# +# NOTICE: git push over http depends on this value. +# If you want be able to push huge amount of data to git repository over http +# you will have to increase this value too. +# # Example of output if you try to push 1GB repo to GitLab over http. # -> git push http://gitlab.... master # diff --git a/db/fixtures/development/01_admin.rb b/db/fixtures/development/01_admin.rb index 004d4cd64a1..bba2fc4b186 100644 --- a/db/fixtures/development/01_admin.rb +++ b/db/fixtures/development/01_admin.rb @@ -3,9 +3,9 @@ Gitlab::Seeder.quiet do s.id = 1 s.name = 'Administrator' s.email = 'admin@example.com' + s.notification_email = 'admin@example.com' s.username = 'root' s.password = '5iveL!fe' - s.password_confirmation = '5iveL!fe' s.admin = true s.projects_limit = 100 s.confirmed_at = DateTime.now diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index ae4c0550a4f..87839770924 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -23,7 +23,7 @@ Sidekiq::Testing.inline! do name: group_path.titleize, path: group_path ) - group.description = Faker::Lorem.sentence + group.description = FFaker::Lorem.sentence group.save group.add_owner(User.first) @@ -35,7 +35,7 @@ Sidekiq::Testing.inline! do import_url: url, namespace_id: group.id, name: project_path.titleize, - description: Faker::Lorem.sentence, + description: FFaker::Lorem.sentence, visibility_level: Gitlab::VisibilityLevel.values.sample } diff --git a/db/fixtures/development/05_users.rb b/db/fixtures/development/05_users.rb index c263dd232af..378354efd5a 100644 --- a/db/fixtures/development/05_users.rb +++ b/db/fixtures/development/05_users.rb @@ -1,31 +1,31 @@ Gitlab::Seeder.quiet do (2..20).each do |i| begin - User.seed(:id, [{ - id: i, - username: Faker::Internet.user_name, - name: Faker::Name.name, - email: Faker::Internet.email, - confirmed_at: DateTime.now - }]) + User.create!( + username: FFaker::Internet.user_name, + name: FFaker::Name.name, + email: FFaker::Internet.email, + confirmed_at: DateTime.now, + password: '12345678' + ) + print '.' - rescue ActiveRecord::RecordNotSaved + rescue ActiveRecord::RecordInvalid print 'F' end end (1..5).each do |i| begin - User.seed(:id, [ - id: i + 10, + User.create!( username: "user#{i}", name: "User #{i}", email: "user#{i}@example.com", confirmed_at: DateTime.now, password: '12345678' - ]) + ) print '.' - rescue ActiveRecord::RecordNotSaved + rescue ActiveRecord::RecordInvalid print 'F' end end diff --git a/db/fixtures/development/07_milestones.rb b/db/fixtures/development/07_milestones.rb index 2296821e528..a43116829d9 100644 --- a/db/fixtures/development/07_milestones.rb +++ b/db/fixtures/development/07_milestones.rb @@ -3,7 +3,7 @@ Gitlab::Seeder.quiet do (1..5).each do |i| milestone_params = { title: "v#{i}.0", - description: Faker::Lorem.sentence, + description: FFaker::Lorem.sentence, state: ['opened', 'closed'].sample, } diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb index e8b01b46d22..c636e96381c 100644 --- a/db/fixtures/development/09_issues.rb +++ b/db/fixtures/development/09_issues.rb @@ -2,8 +2,8 @@ Gitlab::Seeder.quiet do Project.all.each do |project| (1..10).each do |i| issue_params = { - title: Faker::Lorem.sentence(6), - description: Faker::Lorem.sentence, + title: FFaker::Lorem.sentence(6), + description: FFaker::Lorem.sentence, state: ['opened', 'closed'].sample, milestone: project.milestones.sample, assignee: project.team.users.sample diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index f9b2fd8b05f..0825776ffaa 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -10,8 +10,8 @@ Gitlab::Seeder.quiet do params = { source_branch: source_branch, target_branch: target_branch, - title: Faker::Lorem.sentence(6), - description: Faker::Lorem.sentences(3).join(" "), + title: FFaker::Lorem.sentence(6), + description: FFaker::Lorem.sentences(3).join(" "), milestone: project.milestones.sample, assignee: project.team.users.sample } diff --git a/db/fixtures/development/12_snippets.rb b/db/fixtures/development/12_snippets.rb index b3a6f39c7d5..3bd4b442ade 100644 --- a/db/fixtures/development/12_snippets.rb +++ b/db/fixtures/development/12_snippets.rb @@ -28,8 +28,8 @@ eos PersonalSnippet.seed(:id, [{ id: i, author_id: user.id, - title: Faker::Lorem.sentence(3), - file_name: Faker::Internet.domain_word + '.rb', + title: FFaker::Lorem.sentence(3), + file_name: FFaker::Internet.domain_word + '.rb', visibility_level: Gitlab::VisibilityLevel.values.sample, content: content, }]) diff --git a/db/fixtures/development/13_comments.rb b/db/fixtures/development/13_comments.rb index d37be53c7b9..566c0705638 100644 --- a/db/fixtures/development/13_comments.rb +++ b/db/fixtures/development/13_comments.rb @@ -6,7 +6,7 @@ Gitlab::Seeder.quiet do note_params = { noteable_type: 'Issue', noteable_id: issue.id, - note: Faker::Lorem.sentence, + note: FFaker::Lorem.sentence, } Notes::CreateService.new(project, user, note_params).execute @@ -21,7 +21,7 @@ Gitlab::Seeder.quiet do note_params = { noteable_type: 'MergeRequest', noteable_id: mr.id, - note: Faker::Lorem.sentence, + note: FFaker::Lorem.sentence, } Notes::CreateService.new(project, user, note_params).execute diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb index 0755ac714e1..1c8740f6ba9 100644 --- a/db/fixtures/production/001_admin.rb +++ b/db/fixtures/production/001_admin.rb @@ -11,9 +11,8 @@ admin = User.create( name: "Administrator", username: 'root', password: password, - password_confirmation: password, password_expires_at: expire_time, - theme_id: Gitlab::Theme::MARS + theme_id: Gitlab::Themes::APPLICATION_DEFAULT ) diff --git a/db/migrate/20140125162722_add_avatar_to_projects.rb b/db/migrate/20140125162722_add_avatar_to_projects.rb new file mode 100644 index 00000000000..9523ac722f2 --- /dev/null +++ b/db/migrate/20140125162722_add_avatar_to_projects.rb @@ -0,0 +1,5 @@ +class AddAvatarToProjects < ActiveRecord::Migration + def change + add_column :projects, :avatar, :string + end +end diff --git a/db/migrate/20140907220153_serialize_service_properties.rb b/db/migrate/20140907220153_serialize_service_properties.rb index bd75ab1eacb..d45a10465be 100644 --- a/db/migrate/20140907220153_serialize_service_properties.rb +++ b/db/migrate/20140907220153_serialize_service_properties.rb @@ -1,6 +1,9 @@ class SerializeServiceProperties < ActiveRecord::Migration def change - add_column :services, :properties, :text + unless column_exists?(:services, :properties) + add_column :services, :properties, :text + end + Service.reset_column_information associations = @@ -19,18 +22,21 @@ class SerializeServiceProperties < ActiveRecord::Migration :api_version, :jira_issue_transition_id], } - Service.all.each do |service| + Service.find_each(batch_size: 500).each do |service| associations[service.type.to_sym].each do |attribute| service.send("#{attribute}=", service.attributes[attribute.to_s]) end - service.save + + service.save(validate: false) end - remove_column :services, :project_url, :string - remove_column :services, :subdomain, :string - remove_column :services, :room, :string - remove_column :services, :recipients, :text - remove_column :services, :api_key, :string - remove_column :services, :token, :string + if column_exists?(:services, :project_url) + remove_column :services, :project_url, :string + remove_column :services, :subdomain, :string + remove_column :services, :room, :string + remove_column :services, :recipients, :text + remove_column :services, :api_key, :string + remove_column :services, :token, :string + end end end diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb index a8e07033a5d..5836cd6b8db 100644 --- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb +++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb @@ -10,7 +10,7 @@ class MoveSlackServiceToWebhook < ActiveRecord::Migration slack_service.properties.delete('subdomain') # Room is configured on the Slack side slack_service.properties.delete('room') - slack_service.save + slack_service.save(validate: false) end end end diff --git a/db/migrate/20141121161704_add_identity_table.rb b/db/migrate/20141121161704_add_identity_table.rb new file mode 100644 index 00000000000..a85b0426cec --- /dev/null +++ b/db/migrate/20141121161704_add_identity_table.rb @@ -0,0 +1,46 @@ +class AddIdentityTable < ActiveRecord::Migration + def up + create_table :identities do |t| + t.string :extern_uid + t.string :provider + t.references :user + end + + add_index :identities, :user_id + + execute <<eos +INSERT INTO identities (provider, extern_uid, user_id) +SELECT provider, extern_uid, id FROM users +WHERE provider IS NOT NULL +eos + + if index_exists?(:users, ["extern_uid", "provider"]) + remove_index :users, ["extern_uid", "provider"] + end + + remove_column :users, :extern_uid + remove_column :users, :provider + end + + def down + add_column :users, :extern_uid, :string + add_column :users, :provider, :string + + if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' + execute <<eos +UPDATE users u +SET provider = i.provider, extern_uid = i.extern_uid +FROM identities i +WHERE i.user_id = u.id +eos + else + execute "UPDATE users u, identities i SET u.provider = i.provider, u.extern_uid = i.extern_uid WHERE u.id = i.user_id" + end + + drop_table :identities + + unless index_exists?(:users, ["extern_uid", "provider"]) + add_index "users", ["extern_uid", "provider"], name: "index_users_on_extern_uid_and_provider", unique: true, using: :btree + end + end +end diff --git a/db/migrate/20141205134006_add_locked_at_to_merge_request.rb b/db/migrate/20141205134006_add_locked_at_to_merge_request.rb new file mode 100644 index 00000000000..49651c44a82 --- /dev/null +++ b/db/migrate/20141205134006_add_locked_at_to_merge_request.rb @@ -0,0 +1,5 @@ +class AddLockedAtToMergeRequest < ActiveRecord::Migration + def change + add_column :merge_requests, :locked_at, :datetime + end +end diff --git a/db/migrate/20141216155758_create_doorkeeper_tables.rb b/db/migrate/20141216155758_create_doorkeeper_tables.rb new file mode 100644 index 00000000000..af5aa7d8b73 --- /dev/null +++ b/db/migrate/20141216155758_create_doorkeeper_tables.rb @@ -0,0 +1,42 @@ +class CreateDoorkeeperTables < ActiveRecord::Migration + def change + create_table :oauth_applications do |t| + t.string :name, null: false + t.string :uid, null: false + t.string :secret, null: false + t.text :redirect_uri, null: false + t.string :scopes, null: false, default: '' + t.timestamps + end + + add_index :oauth_applications, :uid, unique: true + + create_table :oauth_access_grants do |t| + t.integer :resource_owner_id, null: false + t.integer :application_id, null: false + t.string :token, null: false + t.integer :expires_in, null: false + t.text :redirect_uri, null: false + t.datetime :created_at, null: false + t.datetime :revoked_at + t.string :scopes + end + + add_index :oauth_access_grants, :token, unique: true + + create_table :oauth_access_tokens do |t| + t.integer :resource_owner_id + t.integer :application_id + t.string :token, null: false + t.string :refresh_token + t.integer :expires_in + t.datetime :revoked_at + t.datetime :created_at, null: false + t.string :scopes + end + + add_index :oauth_access_tokens, :token, unique: true + add_index :oauth_access_tokens, :resource_owner_id + add_index :oauth_access_tokens, :refresh_token, unique: true + end +end diff --git a/db/migrate/20141217125223_add_owner_to_application.rb b/db/migrate/20141217125223_add_owner_to_application.rb new file mode 100644 index 00000000000..7d5e6d07d0f --- /dev/null +++ b/db/migrate/20141217125223_add_owner_to_application.rb @@ -0,0 +1,7 @@ +class AddOwnerToApplication < ActiveRecord::Migration + def change + add_column :oauth_applications, :owner_id, :integer, null: true + add_column :oauth_applications, :owner_type, :string, null: true + add_index :oauth_applications, [:owner_id, :owner_type] + end +end
\ No newline at end of file diff --git a/db/migrate/20141223135007_add_import_data_to_project_table.rb b/db/migrate/20141223135007_add_import_data_to_project_table.rb new file mode 100644 index 00000000000..5db78f94cc9 --- /dev/null +++ b/db/migrate/20141223135007_add_import_data_to_project_table.rb @@ -0,0 +1,8 @@ +class AddImportDataToProjectTable < ActiveRecord::Migration + def change + add_column :projects, :import_type, :string + add_column :projects, :import_source, :string + + add_column :users, :github_access_token, :string + end +end 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 new file mode 100644 index 00000000000..70e7272f7f3 --- /dev/null +++ b/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb @@ -0,0 +1,5 @@ +class AddDevelopersCanPushToProtectedBranches < ActiveRecord::Migration + def change + add_column :protected_branches, :developers_can_push, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20150108073740_create_application_settings.rb b/db/migrate/20150108073740_create_application_settings.rb new file mode 100644 index 00000000000..651e35fdf7a --- /dev/null +++ b/db/migrate/20150108073740_create_application_settings.rb @@ -0,0 +1,13 @@ +class CreateApplicationSettings < ActiveRecord::Migration + def change + create_table :application_settings do |t| + t.integer :default_projects_limit + t.boolean :signup_enabled + t.boolean :signin_enabled + t.boolean :gravatar_enabled + t.text :sign_in_text + + t.timestamps + end + end +end 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 new file mode 100644 index 00000000000..aa179ce3a4d --- /dev/null +++ b/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb @@ -0,0 +1,5 @@ +class AddHomePageUrlForApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :home_page_url, :string + end +end diff --git a/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb b/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb new file mode 100644 index 00000000000..c28ba3197ac --- /dev/null +++ b/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb @@ -0,0 +1,5 @@ +class AddGitlabAccessTokenToUser < ActiveRecord::Migration + def change + add_column :users, :gitlab_access_token, :string + end +end diff --git a/db/migrate/20150125163100_add_default_branch_protection_setting.rb b/db/migrate/20150125163100_add_default_branch_protection_setting.rb new file mode 100644 index 00000000000..5020daf55f3 --- /dev/null +++ b/db/migrate/20150125163100_add_default_branch_protection_setting.rb @@ -0,0 +1,5 @@ +class AddDefaultBranchProtectionSetting < ActiveRecord::Migration + def change + add_column :application_settings, :default_branch_protection, :integer, :default => 2 + end +end diff --git a/db/migrate/20150205211843_add_timestamps_to_identities.rb b/db/migrate/20150205211843_add_timestamps_to_identities.rb new file mode 100644 index 00000000000..77cddbfec3b --- /dev/null +++ b/db/migrate/20150205211843_add_timestamps_to_identities.rb @@ -0,0 +1,5 @@ +class AddTimestampsToIdentities < ActiveRecord::Migration + def change + add_timestamps(:identities) + end +end diff --git a/db/migrate/20150206181414_add_index_to_created_at.rb b/db/migrate/20150206181414_add_index_to_created_at.rb new file mode 100644 index 00000000000..fc624fca60d --- /dev/null +++ b/db/migrate/20150206181414_add_index_to_created_at.rb @@ -0,0 +1,16 @@ +class AddIndexToCreatedAt < ActiveRecord::Migration + def change + add_index "users", [:created_at, :id] + add_index "members", [:created_at, :id] + add_index "projects", [:created_at, :id] + add_index "issues", [:created_at, :id] + add_index "merge_requests", [:created_at, :id] + add_index "milestones", [:created_at, :id] + add_index "namespaces", [:created_at, :id] + add_index "notes", [:created_at, :id] + add_index "identities", [:created_at, :id] + add_index "keys", [:created_at, :id] + add_index "web_hooks", [:created_at, :id] + add_index "snippets", [:created_at, :id] + end +end diff --git a/db/migrate/20150206222854_add_notification_email_to_user.rb b/db/migrate/20150206222854_add_notification_email_to_user.rb new file mode 100644 index 00000000000..ab80f7e582f --- /dev/null +++ b/db/migrate/20150206222854_add_notification_email_to_user.rb @@ -0,0 +1,11 @@ +class AddNotificationEmailToUser < ActiveRecord::Migration + def up + add_column :users, :notification_email, :string + + execute "UPDATE users SET notification_email = email" + end + + def down + remove_column :users, :notification_email + end +end diff --git a/db/migrate/20150209222013_add_missing_index.rb b/db/migrate/20150209222013_add_missing_index.rb new file mode 100644 index 00000000000..a816c2e9e8c --- /dev/null +++ b/db/migrate/20150209222013_add_missing_index.rb @@ -0,0 +1,5 @@ +class AddMissingIndex < ActiveRecord::Migration + def change + add_index "services", [:created_at, :id] + end +end diff --git a/db/migrate/20150211172122_add_template_to_service.rb b/db/migrate/20150211172122_add_template_to_service.rb new file mode 100644 index 00000000000..b1bfbc45ee9 --- /dev/null +++ b/db/migrate/20150211172122_add_template_to_service.rb @@ -0,0 +1,5 @@ +class AddTemplateToService < ActiveRecord::Migration + def change + add_column :services, :template, :boolean, default: false + end +end diff --git a/db/migrate/20150211174341_allow_null_in_services_project_id.rb b/db/migrate/20150211174341_allow_null_in_services_project_id.rb new file mode 100644 index 00000000000..68f02812791 --- /dev/null +++ b/db/migrate/20150211174341_allow_null_in_services_project_id.rb @@ -0,0 +1,5 @@ +class AllowNullInServicesProjectId < ActiveRecord::Migration + def change + change_column :services, :project_id, :integer, null: true + end +end 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 new file mode 100644 index 00000000000..a0439172391 --- /dev/null +++ b/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddTwitterSharingEnabledToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :twitter_sharing_enabled, :boolean, default: true + end +end diff --git a/db/migrate/20150213114800_add_hide_no_password_to_user.rb b/db/migrate/20150213114800_add_hide_no_password_to_user.rb new file mode 100644 index 00000000000..685f0844276 --- /dev/null +++ b/db/migrate/20150213114800_add_hide_no_password_to_user.rb @@ -0,0 +1,5 @@ +class AddHideNoPasswordToUser < ActiveRecord::Migration + def change + add_column :users, :hide_no_password, :boolean, default: false + end +end diff --git a/db/migrate/20150213121042_add_password_automatically_set_to_user.rb b/db/migrate/20150213121042_add_password_automatically_set_to_user.rb new file mode 100644 index 00000000000..c3c7c1ffc77 --- /dev/null +++ b/db/migrate/20150213121042_add_password_automatically_set_to_user.rb @@ -0,0 +1,5 @@ +class AddPasswordAutomaticallySetToUser < ActiveRecord::Migration + def change + add_column :users, :password_automatically_set, :boolean, default: false + end +end 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 new file mode 100644 index 00000000000..23ac1b399ec --- /dev/null +++ b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb @@ -0,0 +1,6 @@ +class AddBitbucketAccessTokenAndSecretToUser < ActiveRecord::Migration + def change + add_column :users, :bitbucket_access_token, :string + add_column :users, :bitbucket_access_token_secret, :string + end +end diff --git a/db/migrate/20150219004514_add_events_to_services.rb b/db/migrate/20150219004514_add_events_to_services.rb new file mode 100644 index 00000000000..cf73a0174f4 --- /dev/null +++ b/db/migrate/20150219004514_add_events_to_services.rb @@ -0,0 +1,8 @@ +class AddEventsToServices < ActiveRecord::Migration + def change + add_column :services, :push_events, :boolean, :default => true + add_column :services, :issues_events, :boolean, :default => true + add_column :services, :merge_requests_events, :boolean, :default => true + add_column :services, :tag_push_events, :boolean, :default => true + end +end diff --git a/db/migrate/20150223022001_set_missing_last_activity_at.rb b/db/migrate/20150223022001_set_missing_last_activity_at.rb new file mode 100644 index 00000000000..3f6d4d83474 --- /dev/null +++ b/db/migrate/20150223022001_set_missing_last_activity_at.rb @@ -0,0 +1,8 @@ +class SetMissingLastActivityAt < ActiveRecord::Migration + def up + execute "UPDATE projects SET last_activity_at = updated_at WHERE last_activity_at IS NULL" + end + + def down + end +end diff --git a/db/migrate/20150225065047_add_note_events_to_services.rb b/db/migrate/20150225065047_add_note_events_to_services.rb new file mode 100644 index 00000000000..d54ba9e482f --- /dev/null +++ b/db/migrate/20150225065047_add_note_events_to_services.rb @@ -0,0 +1,5 @@ +class AddNoteEventsToServices < ActiveRecord::Migration + def change + add_column :services, :note_events, :boolean, default: true, null: false + end +end 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 new file mode 100644 index 00000000000..494c3033bff --- /dev/null +++ b/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddRestrictedVisibilityLevelsToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :restricted_visibility_levels, :text + end +end diff --git a/db/migrate/20150306023106_fix_namespace_duplication.rb b/db/migrate/20150306023106_fix_namespace_duplication.rb new file mode 100644 index 00000000000..334e5574559 --- /dev/null +++ b/db/migrate/20150306023106_fix_namespace_duplication.rb @@ -0,0 +1,21 @@ +class FixNamespaceDuplication < ActiveRecord::Migration + def up + #fixes path duplication + select_all('SELECT MAX(id) max, COUNT(id) cnt, path FROM namespaces GROUP BY path HAVING COUNT(id) > 1').each do |nms| + bad_nms_ids = select_all("SELECT id FROM namespaces WHERE path = '#{nms['path']}' AND id <> #{nms['max']}").map{|x| x["id"]} + execute("UPDATE projects SET namespace_id = #{nms["max"]} WHERE namespace_id IN(#{bad_nms_ids.join(', ')})") + execute("DELETE FROM namespaces WHERE id IN(#{bad_nms_ids.join(', ')})") + end + + #fixes name duplication + select_all('SELECT MAX(id) max, COUNT(id) cnt, name FROM namespaces GROUP BY name HAVING COUNT(id) > 1').each do |nms| + bad_nms_ids = select_all("SELECT id FROM namespaces WHERE name = '#{nms['name']}' AND id <> #{nms['max']}").map{|x| x["id"]} + execute("UPDATE projects SET namespace_id = #{nms["max"]} WHERE namespace_id IN(#{bad_nms_ids.join(', ')})") + execute("DELETE FROM namespaces WHERE id IN(#{bad_nms_ids.join(', ')})") + end + end + + def down + # not implemented + end +end diff --git a/db/migrate/20150306023112_add_unique_index_to_namespace.rb b/db/migrate/20150306023112_add_unique_index_to_namespace.rb new file mode 100644 index 00000000000..6472138e3ef --- /dev/null +++ b/db/migrate/20150306023112_add_unique_index_to_namespace.rb @@ -0,0 +1,9 @@ +class AddUniqueIndexToNamespace < ActiveRecord::Migration + def change + remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) + remove_index :namespaces, column: :path if index_exists?(:namespaces, :path) + + add_index :namespaces, :name, unique: true + add_index :namespaces, :path, unique: true + end +end diff --git a/db/migrate/20150310194358_add_version_check_to_application_settings.rb b/db/migrate/20150310194358_add_version_check_to_application_settings.rb new file mode 100644 index 00000000000..e9d42c1e749 --- /dev/null +++ b/db/migrate/20150310194358_add_version_check_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddVersionCheckToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :version_check_enabled, :boolean, default: true + end +end diff --git a/db/migrate/20150313012111_create_subscriptions_table.rb b/db/migrate/20150313012111_create_subscriptions_table.rb new file mode 100644 index 00000000000..a1d4d9dedc5 --- /dev/null +++ b/db/migrate/20150313012111_create_subscriptions_table.rb @@ -0,0 +1,16 @@ +class CreateSubscriptionsTable < ActiveRecord::Migration + def change + create_table :subscriptions do |t| + t.integer :user_id + t.references :subscribable, polymorphic: true + t.boolean :subscribed + + t.timestamps + end + + add_index :subscriptions, + [:subscribable_id, :subscribable_type, :user_id], + unique: true, + name: 'subscriptions_user_id_and_ref_fields' + end +end diff --git a/db/migrate/20150320234437_add_location_to_user.rb b/db/migrate/20150320234437_add_location_to_user.rb new file mode 100644 index 00000000000..32731d37d75 --- /dev/null +++ b/db/migrate/20150320234437_add_location_to_user.rb @@ -0,0 +1,5 @@ +class AddLocationToUser < ActiveRecord::Migration + def change + add_column :users, :location, :string + end +end diff --git a/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb b/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb new file mode 100644 index 00000000000..42dc8173e46 --- /dev/null +++ b/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb @@ -0,0 +1,6 @@ +class SetIncorrectAssigneeIdToNull < ActiveRecord::Migration + def up + execute "UPDATE issues SET assignee_id = NULL WHERE assignee_id = -1" + execute "UPDATE merge_requests SET assignee_id = NULL WHERE assignee_id = -1" + end +end diff --git a/db/migrate/20150327122227_add_public_to_key.rb b/db/migrate/20150327122227_add_public_to_key.rb new file mode 100644 index 00000000000..6ffbf4cda19 --- /dev/null +++ b/db/migrate/20150327122227_add_public_to_key.rb @@ -0,0 +1,5 @@ +class AddPublicToKey < ActiveRecord::Migration + def change + add_column :keys, :public, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20150327150017_add_import_data_to_project.rb b/db/migrate/20150327150017_add_import_data_to_project.rb new file mode 100644 index 00000000000..12c00339eec --- /dev/null +++ b/db/migrate/20150327150017_add_import_data_to_project.rb @@ -0,0 +1,5 @@ +class AddImportDataToProject < ActiveRecord::Migration + def change + add_column :projects, :import_data, :text + end +end diff --git a/db/migrate/20150327223628_add_devise_two_factor_to_users.rb b/db/migrate/20150327223628_add_devise_two_factor_to_users.rb new file mode 100644 index 00000000000..11b026ee8f3 --- /dev/null +++ b/db/migrate/20150327223628_add_devise_two_factor_to_users.rb @@ -0,0 +1,8 @@ +class AddDeviseTwoFactorToUsers < ActiveRecord::Migration + def change + add_column :users, :encrypted_otp_secret, :string + add_column :users, :encrypted_otp_secret_iv, :string + add_column :users, :encrypted_otp_secret_salt, :string + add_column :users, :otp_required_for_login, :boolean + end +end 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 new file mode 100644 index 00000000000..1d161674a9a --- /dev/null +++ b/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddMaxAttachmentSizeToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :max_attachment_size, :integer, default: 10, null: false + end +end 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 new file mode 100644 index 00000000000..913958db7c5 --- /dev/null +++ b/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb @@ -0,0 +1,5 @@ +class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration + def change + add_column :users, :otp_backup_codes, :text + end +end diff --git a/db/migrate/20150406133311_add_invite_data_to_member.rb b/db/migrate/20150406133311_add_invite_data_to_member.rb new file mode 100644 index 00000000000..5d3e856ddce --- /dev/null +++ b/db/migrate/20150406133311_add_invite_data_to_member.rb @@ -0,0 +1,23 @@ +class AddInviteDataToMember < ActiveRecord::Migration + def up + add_column :members, :created_by_id, :integer + add_column :members, :invite_email, :string + add_column :members, :invite_token, :string + add_column :members, :invite_accepted_at, :datetime + + change_column :members, :user_id, :integer, null: true + + add_index :members, :invite_token, unique: true + end + + def down + remove_index :members, :invite_token + + change_column :members, :user_id, :integer, null: false + + remove_column :members, :invite_accepted_at + remove_column :members, :invite_token + remove_column :members, :invite_email + remove_column :members, :created_by_id + end +end diff --git a/db/migrate/20150411000035_fix_identities.rb b/db/migrate/20150411000035_fix_identities.rb new file mode 100644 index 00000000000..d9051f9fffd --- /dev/null +++ b/db/migrate/20150411000035_fix_identities.rb @@ -0,0 +1,45 @@ +class FixIdentities < ActiveRecord::Migration + def up + # Up until now, legacy 'ldap' references in the database were charitably + # interpreted to point to the first LDAP server specified in the GitLab + # configuration. So if the database said 'provider: ldap' but the first + # LDAP server was called 'ldapmain', then we would try to interpret + # 'provider: ldap' as if it said 'provider: ldapmain'. This migration (and + # accompanying changes in the GitLab LDAP code) get rid of this complicated + # behavior. Any database references to 'provider: ldap' get rewritten to + # whatever the code would have interpreted it as, i.e. as a reference to + # the first LDAP server specified in gitlab.yml / gitlab.rb. + new_provider = if Gitlab.config.ldap.enabled + first_ldap_server = Gitlab.config.ldap.servers.values.first + first_ldap_server['provider_name'] + else + 'ldapmain' + end + + # Delete duplicate identities + # We use a sort of self-join to find rows in identities which match on + # user_id but where one has provider 'ldap'. We delete the duplicate row + # with provider 'ldap'. + delete_statement = '' + case adapter_name.downcase + when /^mysql/ + delete_statement << 'DELETE FROM id1 USING identities AS id1, identities AS id2' + when 'postgresql' + delete_statement << 'DELETE FROM identities AS id1 USING identities AS id2' + else + raise "Unknown DB adapter: #{adapter_name}" + end + delete_statement << " WHERE id1.user_id = id2.user_id AND id1.provider = 'ldap' AND id2.provider = '#{new_provider}'" + execute delete_statement + + # Update legacy identities + execute "UPDATE identities SET provider = '#{new_provider}' WHERE provider = 'ldap'" + + if table_exists?('ldap_group_links') + execute "UPDATE ldap_group_links SET provider = '#{new_provider}' WHERE provider IS NULL OR provider = 'ldap'" + end + end + + def down + end +end diff --git a/db/migrate/20150411180045_rename_buildbox_service.rb b/db/migrate/20150411180045_rename_buildbox_service.rb new file mode 100644 index 00000000000..5a0b5d07e50 --- /dev/null +++ b/db/migrate/20150411180045_rename_buildbox_service.rb @@ -0,0 +1,9 @@ +class RenameBuildboxService < ActiveRecord::Migration + def up + execute "UPDATE services SET type = 'BuildkiteService' WHERE type = 'BuildboxService';" + end + + def down + execute "UPDATE services SET type = 'BuildboxService' WHERE type = 'BuildkiteService';" + end +end diff --git a/db/migrate/20150413192223_add_public_email_to_users.rb b/db/migrate/20150413192223_add_public_email_to_users.rb new file mode 100644 index 00000000000..700e9f343a6 --- /dev/null +++ b/db/migrate/20150413192223_add_public_email_to_users.rb @@ -0,0 +1,5 @@ +class AddPublicEmailToUsers < ActiveRecord::Migration + def change + add_column :users, :public_email, :string, default: "", null: false + end +end diff --git a/db/migrate/20150417121913_create_project_import_data.rb b/db/migrate/20150417121913_create_project_import_data.rb new file mode 100644 index 00000000000..c78f5fde85e --- /dev/null +++ b/db/migrate/20150417121913_create_project_import_data.rb @@ -0,0 +1,8 @@ +class CreateProjectImportData < ActiveRecord::Migration + def change + create_table :project_import_data do |t| + t.references :project + t.text :data + end + end +end diff --git a/db/migrate/20150417122318_remove_import_data_from_project.rb b/db/migrate/20150417122318_remove_import_data_from_project.rb new file mode 100644 index 00000000000..46cf63593c9 --- /dev/null +++ b/db/migrate/20150417122318_remove_import_data_from_project.rb @@ -0,0 +1,9 @@ +class RemoveImportDataFromProject < ActiveRecord::Migration + def up + remove_column :projects, :import_data + end + + def down + add_column :projects, :import_data, :text + end +end diff --git a/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb b/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb new file mode 100644 index 00000000000..3057ea3c68c --- /dev/null +++ b/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb @@ -0,0 +1,88 @@ +class RemovePeriodsAtEndsOfUsernames < ActiveRecord::Migration + include Gitlab::ShellAdapter + + class Namespace < ActiveRecord::Base + class << self + def find_by_path_or_name(path) + find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) + end + + def clean_path(path) + path = path.dup + # Get the email username by removing everything after an `@` sign. + path.gsub!(/@.*\z/, "") + # Usernames can't end in .git, so remove it. + path.gsub!(/\.git\z/, "") + # Remove dashes at the start of the username. + path.gsub!(/\A-+/, "") + # Remove periods at the end of the username. + path.gsub!(/\.+\z/, "") + # Remove everything that's not in the list of allowed characters. + path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + + # Users with the great usernames of "." or ".." would end up with a blank username. + # Work around that by setting their username to "blank", followed by a counter. + path = "blank" if path.blank? + + counter = 0 + base = path + while Namespace.find_by_path_or_name(path) + counter += 1 + path = "#{base}#{counter}" + end + + path + end + end + end + + def up + changed_paths = {} + + select_all("SELECT id, username FROM users WHERE username LIKE '%.'").each do |user| + username_was = user["username"] + username = Namespace.clean_path(username_was) + changed_paths[username_was] = username + + username = quote_string(username) + execute "UPDATE users SET username = '#{username}' WHERE id = #{user["id"]}" + execute "UPDATE namespaces SET path = '#{username}', name = '#{username}' WHERE type IS NULL AND owner_id = #{user["id"]}" + end + + select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.'").each do |group| + path_was = group["path"] + path = Namespace.clean_path(path_was) + changed_paths[path_was] = path + + path = quote_string(path) + execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group["id"]}" + end + + changed_paths.each do |path_was, path| + # Don't attempt to move if original path only contains periods. + next if path_was =~ /\A\.+\z/ + + if gitlab_shell.mv_namespace(path_was, path) + # If repositories moved successfully we need to remove old satellites + # and send update instructions to users. + # However we cannot allow rollback since we moved namespace dir + # So we basically we mute exceptions in next actions + begin + gitlab_shell.rm_satellites(path_was) + # We cannot send update instructions since models and mailers + # can't safely be used from migrations as they may be written for + # later versions of the database. + # send_update_instructions + rescue + # Returning false does not rollback after_* transaction but gives + # us information about failing some of tasks + false + end + else + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Exception.new('namespace directory cannot be moved') + end + end + end +end 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 new file mode 100644 index 00000000000..50a9b2439e0 --- /dev/null +++ b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb @@ -0,0 +1,11 @@ +class AddDefaultProjectVisibililtyToApplicationSettings < ActiveRecord::Migration + def up + add_column :application_settings, :default_project_visibility, :integer + visibility = Settings.gitlab.default_projects_features['visibility_level'] + execute("update application_settings set default_project_visibility = #{visibility}") + end + + def down + remove_column :application_settings, :default_project_visibility + end +end 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 new file mode 100644 index 00000000000..281c88d2a7d --- /dev/null +++ b/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb @@ -0,0 +1,10 @@ +# 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. + +class GitlabChangeCollationForTagNames < ActiveRecord::Migration + def up + if ActsAsTaggableOn::Utils.using_mysql? + execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;") + end + end +end diff --git a/db/migrate/20150425164647_remove_duplicate_tags.rb b/db/migrate/20150425164647_remove_duplicate_tags.rb new file mode 100644 index 00000000000..13e5038db9c --- /dev/null +++ b/db/migrate/20150425164647_remove_duplicate_tags.rb @@ -0,0 +1,17 @@ +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| + tag_name = quote_string(tag["name"]) + duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]} + origin_tag_id = duplicate_ids.first + duplicate_ids.delete origin_tag_id + + 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 + end + + def down + + end +end 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 new file mode 100644 index 00000000000..c1b78681519 --- /dev/null +++ b/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb @@ -0,0 +1,27 @@ +# This migration comes from acts_as_taggable_on_engine (originally 2) +class AddMissingUniqueIndices < ActiveRecord::Migration + def self.up + add_index :tags, :name, unique: true + + # pre-GitLab v6.7.0 may not have these indices since there were no + # migrations for them + if index_exists?(:taggings, :tag_id) + remove_index :taggings, :tag_id + end + + if index_exists?(:taggings, [:taggable_id, :taggable_type, :context]) + remove_index :taggings, [:taggable_id, :taggable_type, :context] + end + add_index :taggings, + [:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type], + unique: true, name: 'taggings_idx' + end + + def self.down + remove_index :tags, :name + + remove_index :taggings, name: 'taggings_idx' + add_index :taggings, :tag_id + add_index :taggings, [:taggable_id, :taggable_type, :context] + end +end 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 new file mode 100644 index 00000000000..8edb5080781 --- /dev/null +++ b/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb @@ -0,0 +1,15 @@ +# This migration comes from acts_as_taggable_on_engine (originally 3) +class AddTaggingsCounterCacheToTags < ActiveRecord::Migration + def self.up + add_column :tags, :taggings_count, :integer, default: 0 + + ActsAsTaggableOn::Tag.reset_column_information + ActsAsTaggableOn::Tag.find_each do |tag| + ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings) + end + end + + def self.down + remove_column :tags, :taggings_count + end +end 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 new file mode 100644 index 00000000000..71f2d7f4330 --- /dev/null +++ b/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb @@ -0,0 +1,10 @@ +# This migration comes from acts_as_taggable_on_engine (originally 4) +class AddMissingTaggableIndex < ActiveRecord::Migration + def self.up + add_index :taggings, [:taggable_id, :taggable_type, :context] + end + + def self.down + remove_index :taggings, [:taggable_id, :taggable_type, :context] + end +end 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 new file mode 100644 index 00000000000..bfb06bc7cda --- /dev/null +++ b/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb @@ -0,0 +1,10 @@ +# 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 +class ChangeCollationForTagNames < ActiveRecord::Migration + def up + if ActsAsTaggableOn::Utils.using_mysql? + execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;") + end + end +end 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 new file mode 100644 index 00000000000..8f1b0cc8935 --- /dev/null +++ b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb @@ -0,0 +1,11 @@ +class AddDefaultSnippetVisibilityToAppSettings < ActiveRecord::Migration + def up + add_column :application_settings, :default_snippet_visibility, :integer + visibility = Settings.gitlab.default_projects_features['visibility_level'] + execute("update application_settings set default_snippet_visibility = #{visibility}") + end + + def down + remove_column :application_settings, :default_snippet_visibility + end +end diff --git a/db/migrate/20150429002313_remove_abandoned_group_members_records.rb b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb new file mode 100644 index 00000000000..244637e1c4a --- /dev/null +++ b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb @@ -0,0 +1,9 @@ +class RemoveAbandonedGroupMembersRecords < ActiveRecord::Migration + def up + execute("DELETE FROM members WHERE type = 'GroupMember' AND source_id NOT IN(\ + SELECT id FROM namespaces WHERE type='Group')") + end + + def down + end +end 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 new file mode 100644 index 00000000000..184e2653610 --- /dev/null +++ b/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddRestrictedSignupDomainsToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :restricted_signup_domains, :text + end +end diff --git a/db/migrate/20150509180749_convert_legacy_reference_notes.rb b/db/migrate/20150509180749_convert_legacy_reference_notes.rb new file mode 100644 index 00000000000..b02605489be --- /dev/null +++ b/db/migrate/20150509180749_convert_legacy_reference_notes.rb @@ -0,0 +1,16 @@ +# Convert legacy Markdown-emphasized notes to the current, non-emphasized format +# +# _mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666_ +# +# becomes +# +# mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666 +class ConvertLegacyReferenceNotes < ActiveRecord::Migration + def up + execute %q{UPDATE notes SET note = trim(both '_' from note) WHERE system = true AND note LIKE '\_%\_'} + end + + def down + # noop + end +end diff --git a/db/migrate/20150516060434_add_note_events_to_web_hooks.rb b/db/migrate/20150516060434_add_note_events_to_web_hooks.rb new file mode 100644 index 00000000000..0097587b4f6 --- /dev/null +++ b/db/migrate/20150516060434_add_note_events_to_web_hooks.rb @@ -0,0 +1,9 @@ +class AddNoteEventsToWebHooks < ActiveRecord::Migration + def up + add_column :web_hooks, :note_events, :boolean, default: false, null: false + end + + def down + remove_column :web_hooks, :note_events, :boolean + end +end 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 new file mode 100644 index 00000000000..6a78294f0b2 --- /dev/null +++ b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddUserOauthApplicationsToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :user_oauth_applications, :bool, default: true + end +end 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 new file mode 100644 index 00000000000..83e08101407 --- /dev/null +++ b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb @@ -0,0 +1,5 @@ +class AddAfterSignOutPathForApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :after_sign_out_path, :string + end +end
\ No newline at end of file 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 new file mode 100644 index 00000000000..ffa22e6d5ef --- /dev/null +++ b/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb @@ -0,0 +1,5 @@ +class AddSessionExpireDelayForApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :session_expire_delay, :integer, default: 10080, null: false + end +end
\ No newline at end of file diff --git a/db/migrate/20150610065936_add_dashboard_to_users.rb b/db/migrate/20150610065936_add_dashboard_to_users.rb new file mode 100644 index 00000000000..2628e450722 --- /dev/null +++ b/db/migrate/20150610065936_add_dashboard_to_users.rb @@ -0,0 +1,9 @@ +class AddDashboardToUsers < ActiveRecord::Migration + def up + add_column :users, :dashboard, :integer, default: 0 + end + + def down + remove_column :users, :dashboard + end +end diff --git a/db/schema.rb b/db/schema.rb index 68d1080b6ee..f063a4868b1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,11 +11,33 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20141121133009) do +ActiveRecord::Schema.define(version: 20150610065936) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "application_settings", force: true do |t| + t.integer "default_projects_limit" + t.boolean "signup_enabled" + t.boolean "signin_enabled" + t.boolean "gravatar_enabled" + t.text "sign_in_text" + t.datetime "created_at" + t.datetime "updated_at" + t.string "home_page_url" + t.integer "default_branch_protection", default: 2 + t.boolean "twitter_sharing_enabled", default: true + t.text "restricted_visibility_levels" + 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.string "after_sign_out_path" + t.integer "session_expire_delay", default: 10080, null: false + end + create_table "broadcast_messages", force: true do |t| t.text "message", null: false t.datetime "starts_at" @@ -74,6 +96,17 @@ ActiveRecord::Schema.define(version: 20141121133009) do add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree + create_table "identities", force: true do |t| + t.string "extern_uid" + t.string "provider" + t.integer "user_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "identities", ["created_at", "id"], name: "index_identities_on_created_at_and_id", using: :btree + add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree + create_table "issues", force: true do |t| t.string "title" t.integer "assignee_id" @@ -91,6 +124,7 @@ ActiveRecord::Schema.define(version: 20141121133009) do add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree + add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree @@ -105,8 +139,10 @@ ActiveRecord::Schema.define(version: 20141121133009) do t.string "title" t.string "type" t.string "fingerprint" + t.boolean "public", default: false, null: false end + add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree create_table "label_links", force: true do |t| @@ -134,14 +170,20 @@ ActiveRecord::Schema.define(version: 20141121133009) do t.integer "access_level", null: false t.integer "source_id", null: false t.string "source_type", null: false - t.integer "user_id", null: false + t.integer "user_id" t.integer "notification_level", null: false t.string "type" t.datetime "created_at" t.datetime "updated_at" + t.integer "created_by_id" + t.string "invite_email" + t.string "invite_token" + t.datetime "invite_accepted_at" end add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree + add_index "members", ["created_at", "id"], name: "index_members_on_created_at_and_id", using: :btree + add_index "members", ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree add_index "members", ["type"], name: "index_members_on_type", using: :btree add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree @@ -173,10 +215,12 @@ ActiveRecord::Schema.define(version: 20141121133009) do t.integer "iid" t.text "description" t.integer "position", default: 0 + t.datetime "locked_at" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree + add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree @@ -196,6 +240,7 @@ ActiveRecord::Schema.define(version: 20141121133009) do t.integer "iid" end + add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree @@ -211,9 +256,10 @@ ActiveRecord::Schema.define(version: 20141121133009) do t.string "avatar" end - add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree + add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree + add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree - add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree + add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree create_table "notes", force: true do |t| @@ -233,6 +279,7 @@ ActiveRecord::Schema.define(version: 20141121133009) do 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", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree @@ -240,6 +287,54 @@ ActiveRecord::Schema.define(version: 20141121133009) do add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree + create_table "oauth_access_grants", force: true do |t| + t.integer "resource_owner_id", null: false + t.integer "application_id", null: false + t.string "token", null: false + t.integer "expires_in", null: false + t.text "redirect_uri", null: false + t.datetime "created_at", null: false + t.datetime "revoked_at" + t.string "scopes" + end + + add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree + + create_table "oauth_access_tokens", force: true do |t| + t.integer "resource_owner_id" + t.integer "application_id" + t.string "token", null: false + t.string "refresh_token" + t.integer "expires_in" + t.datetime "revoked_at" + t.datetime "created_at", null: false + t.string "scopes" + end + + add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree + add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree + add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree + + create_table "oauth_applications", force: true do |t| + t.string "name", null: false + t.string "uid", null: false + t.string "secret", null: false + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.integer "owner_id" + t.string "owner_type" + end + + add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree + add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree + + create_table "project_import_data", force: true do |t| + t.integer "project_id" + t.text "data" + end + create_table "projects", force: true do |t| t.string "name" t.string "path" @@ -259,21 +354,26 @@ ActiveRecord::Schema.define(version: 20141121133009) do t.string "import_url" 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.string "import_type" + t.string "import_source" end + add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree create_table "protected_branches", force: true do |t| - t.integer "project_id", null: false - t.string "name", null: false + t.integer "project_id", null: false + t.string "name", null: false t.datetime "created_at" t.datetime "updated_at" + t.boolean "developers_can_push", default: false, null: false end add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree @@ -281,13 +381,20 @@ ActiveRecord::Schema.define(version: 20141121133009) do create_table "services", force: true do |t| t.string "type" t.string "title" - t.integer "project_id", null: false + t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.boolean "active", default: false, null: false t.text "properties" + t.boolean "template", default: false + t.boolean "push_events", default: true + t.boolean "issues_events", default: true + t.boolean "merge_requests_events", default: true + t.boolean "tag_push_events", default: true + t.boolean "note_events", default: true, null: false end + add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree create_table "snippets", force: true do |t| @@ -304,11 +411,23 @@ ActiveRecord::Schema.define(version: 20141121133009) do end add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree + add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree + create_table "subscriptions", force: true do |t| + t.integer "user_id" + t.integer "subscribable_id" + t.string "subscribable_type" + t.boolean "subscribed" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree + create_table "taggings", force: true do |t| t.integer "tag_id" t.integer "taggable_id" @@ -319,20 +438,23 @@ ActiveRecord::Schema.define(version: 20141121133009) do t.datetime "created_at" end - add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree + add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true, using: :btree add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree create_table "tags", force: true do |t| - t.string "name" + t.string "name" + t.integer "taggings_count", default: 0 end + add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree + create_table "users", force: true do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" t.string "current_sign_in_ip" @@ -340,42 +462,55 @@ ActiveRecord::Schema.define(version: 20141121133009) do t.datetime "created_at" t.datetime "updated_at" t.string "name" - t.boolean "admin", default: false, null: false - t.integer "projects_limit", default: 10 - t.string "skype", default: "", null: false - t.string "linkedin", default: "", null: false - t.string "twitter", default: "", null: false + t.boolean "admin", default: false, null: false + t.integer "projects_limit", default: 10 + t.string "skype", default: "", null: false + t.string "linkedin", default: "", null: false + t.string "twitter", default: "", null: false t.string "authentication_token" - t.integer "theme_id", default: 1, null: false + t.integer "theme_id", default: 1, null: false t.string "bio" - t.integer "failed_attempts", default: 0 + t.integer "failed_attempts", default: 0 t.datetime "locked_at" - t.string "extern_uid" - t.string "provider" t.string "username" - t.boolean "can_create_group", default: true, null: false - t.boolean "can_create_team", default: true, null: false + t.boolean "can_create_group", default: true, null: false + t.boolean "can_create_team", default: true, null: false t.string "state" - t.integer "color_scheme_id", default: 1, null: false - t.integer "notification_level", default: 1, null: false + t.integer "color_scheme_id", default: 1, null: false + t.integer "notification_level", default: 1, null: false t.datetime "password_expires_at" t.integer "created_by_id" + t.datetime "last_credential_check_at" t.string "avatar" t.string "confirmation_token" t.datetime "confirmed_at" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" - t.boolean "hide_no_ssh_key", default: false - t.string "website_url", default: "", null: false - t.datetime "last_credential_check_at" + t.boolean "hide_no_ssh_key", default: false + t.string "website_url", default: "", null: false + t.string "github_access_token" + t.string "gitlab_access_token" + t.string "notification_email" + t.boolean "hide_no_password", default: false + t.boolean "password_automatically_set", default: false + t.string "bitbucket_access_token" + t.string "bitbucket_access_token_secret" + t.string "location" + t.string "encrypted_otp_secret" + t.string "encrypted_otp_secret_iv" + t.string "encrypted_otp_secret_salt" + t.boolean "otp_required_for_login" + t.text "otp_backup_codes" + t.string "public_email", default: "", null: false + t.integer "dashboard", default: 0 end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree + add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree - add_index "users", ["extern_uid", "provider"], name: "index_users_on_extern_uid_and_provider", unique: true, using: :btree add_index "users", ["name"], name: "index_users_on_name", using: :btree add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree @@ -402,8 +537,10 @@ ActiveRecord::Schema.define(version: 20141121133009) do t.boolean "issues_events", default: false, null: false t.boolean "merge_requests_events", default: false, null: false t.boolean "tag_push_events", default: false + t.boolean "note_events", default: false, null: false end + 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 end diff --git a/doc/README.md b/doc/README.md index 896224fe930..28459613252 100644 --- a/doc/README.md +++ b/doc/README.md @@ -3,26 +3,31 @@ ## User documentation - [API](api/README.md) Automate GitLab via a simple and powerful API. +- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. +- [Importing to GitLab](workflow/importing/README.md). - [Markdown](markdown/markdown.md) GitLab's advanced formatting system. - [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do. +- [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. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. -- [Workflow](workflow/README.md) Learn how to get the maximum out of GitLab. +- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. ## Administrator documentation -- [Install](install/README.md) Requirements, directory structures and manual installation. +- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. +- [Install](install/README.md) Requirements, directory structures and installation from source. - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. +- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. +- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. +- [Log system](logs/logs.md) Log system. +- [Operations](operations/README.md) Keeping GitLab up and running - [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects. -- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. -- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. - [Security](security/README.md) Learn what you can do to further secure your GitLab instance. +- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. - [Update](update/README.md) Update guides to upgrade your installation. - [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page. -- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. -- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. ## Contributor documentation diff --git a/doc/api/README.md b/doc/api/README.md index ffe250df3ff..ca58c184543 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -6,6 +6,7 @@ - [Session](session.md) - [Projects](projects.md) - [Project Snippets](project_snippets.md) +- [Services](services.md) - [Repositories](repositories.md) - [Repository Files](repository_files.md) - [Commits](commits.md) @@ -18,10 +19,12 @@ - [Deploy Keys](deploy_keys.md) - [System Hooks](system_hooks.md) - [Groups](groups.md) +- [Namespaces](namespaces.md) ## Clients Find API Clients for GitLab [on our website](https://about.gitlab.com/applications/#api-clients). +You can use [GitLab as an OAuth2 client](oauth2.md) to make API calls. ## Introduction @@ -51,6 +54,24 @@ curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://example.com/api/v3/p The API uses JSON to serialize data. You don't need to specify `.json` at the end of API URL. +## Authentication with OAuth2 token + +Instead of the private_token you can transmit the OAuth2 access token as a header or as a parameter. + +### OAuth2 token (as a parameter) + +``` +curl https://localhost:3000/api/v3/user?access_token=OAUTH-TOKEN +``` + +### OAuth2 token (as a header) + +``` +curl -H "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user +``` + +Read more about [GitLab as an OAuth2 client](oauth2.md). + ## Status codes The API is designed to return different status codes according to context and action. In this way if a request results in an error the caller is able to get insight into what went wrong, e.g. status code `400 Bad Request` is returned if a required attribute is missing from the request. The following list gives an overview of how the API functions generally behave. @@ -79,7 +100,7 @@ Return values: ## Sudo -All API requests support performing an api call as if you were another user, if your private token is for an administration account. You need to pass `sudo` parameter by url or header with an id or username of the user you want to perform the operation as. If passed as header, the header name must be "SUDO" (capitals). +All API requests support performing an api call as if you were another user, if your private token is for an administration account. You need to pass `sudo` parameter by URL or header with an id or username of the user you want to perform the operation as. If passed as header, the header name must be "SUDO" (capitals). If a non administrative `private_token` is provided then an error message will be returned with status code 403: @@ -124,7 +145,7 @@ When listing resources you can pass the following parameters: - `page` (default: `1`) - page number - `per_page` (default: `20`, max: `100`) - number of items to list per page -[Link headers](http://www.w3.org/wiki/LinkHeader) are send back with each response. These have `rel` prev/next/first/last and contain the relevant URL. Please use these instead of generating your own urls. +[Link headers](http://www.w3.org/wiki/LinkHeader) are send back with each response. These have `rel` prev/next/first/last and contain the relevant URL. Please use these instead of generating your own URLs. ## id vs iid diff --git a/doc/api/branches.md b/doc/api/branches.md index 319f0b47386..6a9c10c8520 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -15,27 +15,20 @@ Parameters: ```json [ { - "name": "master", "commit": { + "author_email": "john@example.com", + "author_name": "John Smith", + "authored_date": "2012-06-27T05:51:39-07:00", + "committed_date": "2012-06-28T03:44:20-07:00", + "committer_email": "john@example.com", + "committer_name": "John Smith", "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "parents": [ - { - "id": "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - } - ], - "tree": "46e82de44b1061621357f24c05515327f2795a95", "message": "add projects API", - "author": { - "name": "John Smith", - "email": "john@example.com" - }, - "committer": { - "name": "John Smith", - "email": "john@example.com" - }, - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00" + "parent_ids": [ + "4ad91d3c1144c406e50c7b33bae684bd6837faf8" + ] }, + "name": "master", "protected": true } ] @@ -56,27 +49,20 @@ Parameters: ```json { - "name": "master", "commit": { + "author_email": "john@example.com", + "author_name": "John Smith", + "authored_date": "2012-06-27T05:51:39-07:00", + "committed_date": "2012-06-28T03:44:20-07:00", + "committer_email": "john@example.com", + "committer_name": "John Smith", "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "parents": [ - { - "id": "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - } - ], - "tree": "46e82de44b1061621357f24c05515327f2795a95", "message": "add projects API", - "author": { - "name": "John Smith", - "email": "john@example.com" - }, - "committer": { - "name": "John Smith", - "email": "john@example.com" - }, - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00" + "parent_ids": [ + "4ad91d3c1144c406e50c7b33bae684bd6837faf8" + ] }, + "name": "master", "protected": true } ``` @@ -97,27 +83,20 @@ Parameters: ```json { - "name": "master", "commit": { + "author_email": "john@example.com", + "author_name": "John Smith", + "authored_date": "2012-06-27T05:51:39-07:00", + "committed_date": "2012-06-28T03:44:20-07:00", + "committer_email": "john@example.com", + "committer_name": "John Smith", "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "parents": [ - { - "id": "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - } - ], - "tree": "46e82de44b1061621357f24c05515327f2795a95", "message": "add projects API", - "author": { - "name": "John Smith", - "email": "john@example.com" - }, - "committer": { - "name": "John Smith", - "email": "john@example.com" - }, - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00" + "parent_ids": [ + "4ad91d3c1144c406e50c7b33bae684bd6837faf8" + ] }, + "name": "master", "protected": true } ``` @@ -138,27 +117,20 @@ Parameters: ```json { - "name": "master", "commit": { + "author_email": "john@example.com", + "author_name": "John Smith", + "authored_date": "2012-06-27T05:51:39-07:00", + "committed_date": "2012-06-28T03:44:20-07:00", + "committer_email": "john@example.com", + "committer_name": "John Smith", "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "parents": [ - { - "id": "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - } - ], - "tree": "46e82de44b1061621357f24c05515327f2795a95", "message": "add projects API", - "author": { - "name": "John Smith", - "email": "john@example.com" - }, - "committer": { - "name": "John Smith", - "email": "john@example.com" - }, - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00" + "parent_ids": [ + "4ad91d3c1144c406e50c7b33bae684bd6837faf8" + ] }, + "name": "master", "protected": false } ``` @@ -177,21 +149,20 @@ Parameters: ```json { - "name": "my-new-branch", "commit": { - "id": "8848c0e90327a0b70f1865b843fb2fbfb9345e57", - "message": "Merge pull request #54 from brightbox/use_fog_brightbox_module\n\nUpdate to use fog-brightbox module", - "parent_ids": [ - "fff449e0bf453576f16c91d6544f00a2664009d8", - "f93a93626fec20fd659f4ed3ab2e64019b6169ae" - ], - "authored_date": "2014-02-20T19:54:55+02:00", - "author_name": "john smith", "author_email": "john@example.com", - "committed_date": "2014-02-20T19:54:55+02:00", - "committer_name": "john smith", - "committer_email": "john@example.com" + "author_name": "John Smith", + "authored_date": "2012-06-27T05:51:39-07:00", + "committed_date": "2012-06-28T03:44:20-07:00", + "committer_email": "john@example.com", + "committer_name": "John Smith", + "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", + "message": "add projects API", + "parent_ids": [ + "4ad91d3c1144c406e50c7b33bae684bd6837faf8" + ] }, + "name": "master", "protected": false } ``` diff --git a/doc/api/groups.md b/doc/api/groups.md index 6b379b02d28..0b9f6406d8d 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -1,142 +1,192 @@ -# Groups - -## List project groups - -Get a list of groups. (As user: my groups, as admin: all groups) - -``` -GET /groups -``` - -```json -[ - { - "id": 1, - "name": "Foobar Group", - "path": "foo-bar", - "owner_id": 18 - } -] -``` - -## Details of a group - -Get all details of a group. - -``` -GET /groups/:id -``` - -Parameters: - -- `id` (required) - The ID of a group - -## New group - -Creates a new project group. Available only for admin. - -``` -POST /groups -``` - -Parameters: - -- `name` (required) - The name of the group -- `path` (required) - The path of the group - -## Transfer project to group - -Transfer a project to the Group namespace. Available only for admin - -``` -POST /groups/:id/projects/:project_id -``` - -Parameters: - -- `id` (required) - The ID of a group -- `project_id` (required) - The ID of a project - -## Remove group - -Removes group with all projects inside. - -``` -DELETE /groups/:id -``` - -Parameters: - -- `id` (required) - The ID of a user group - -## Group members - -**Group access levels** - -The group access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized: - -``` -GUEST = 10 -REPORTER = 20 -DEVELOPER = 30 -MASTER = 40 -OWNER = 50 -``` - -### List group members - -Get a list of group members viewable by the authenticated user. - -``` -GET /groups/:id/members -``` - -```json -[ - { - "id": 1, - "username": "raymond_smith", - "email": "ray@smith.org", - "name": "Raymond Smith", - "state": "active", - "created_at": "2012-10-22T14:13:35Z", - "access_level": 30 - }, - { - "id": 2, - "username": "john_doe", - "email": "joh@doe.org", - "name": "John Doe", - "state": "active", - "created_at": "2012-10-22T14:13:35Z", - "access_level": 30 - } -] -``` - -### Add group member - -Adds a user to the list of group members. - -``` -POST /groups/:id/members -``` - -Parameters: - -- `id` (required) - The ID of a group -- `user_id` (required) - The ID of a user to add -- `access_level` (required) - Project access level - -### Remove user team member - -Removes user from user team. - -``` -DELETE /groups/:id/members/:user_id -``` - -Parameters: - -- `id` (required) - The ID of a user group -- `user_id` (required) - The ID of a group member +# Groups
+
+## List project groups
+
+Get a list of groups. (As user: my groups, as admin: all groups)
+
+```
+GET /groups
+```
+
+```json
+[
+ {
+ "id": 1,
+ "name": "Foobar Group",
+ "path": "foo-bar",
+ "description": "An interesting group"
+ }
+]
+```
+
+You can search for groups by name or path, see below.
+
+## Details of a group
+
+Get all details of a group.
+
+```
+GET /groups/:id
+```
+
+Parameters:
+
+- `id` (required) - The ID or path of a group
+
+## New group
+
+Creates a new project group. Available only for users who can create groups.
+
+```
+POST /groups
+```
+
+Parameters:
+
+- `name` (required) - The name of the group
+- `path` (required) - The path of the group
+- `description` (optional) - The group's description
+
+## Transfer project to group
+
+Transfer a project to the Group namespace. Available only for admin
+
+```
+POST /groups/:id/projects/:project_id
+```
+
+Parameters:
+
+- `id` (required) - The ID or path of a group
+- `project_id` (required) - The ID of a project
+
+## Remove group
+
+Removes group with all projects inside.
+
+```
+DELETE /groups/:id
+```
+
+Parameters:
+
+- `id` (required) - The ID or path of a user group
+
+## Search for group
+
+Get all groups that match your string in their name or path.
+
+```
+GET /groups?search=foobar
+```
+
+```json
+[
+ {
+ "id": 1,
+ "name": "Foobar Group",
+ "path": "foo-bar",
+ "description": "An interesting group"
+ }
+]
+```
+
+## Group members
+
+**Group access levels**
+
+The group access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
+
+```
+GUEST = 10
+REPORTER = 20
+DEVELOPER = 30
+MASTER = 40
+OWNER = 50
+```
+
+### List group members
+
+Get a list of group members viewable by the authenticated user.
+
+```
+GET /groups/:id/members
+```
+
+```json
+[
+ {
+ "id": 1,
+ "username": "raymond_smith",
+ "email": "ray@smith.org",
+ "name": "Raymond Smith",
+ "state": "active",
+ "created_at": "2012-10-22T14:13:35Z",
+ "access_level": 30
+ },
+ {
+ "id": 2,
+ "username": "john_doe",
+ "email": "joh@doe.org",
+ "name": "John Doe",
+ "state": "active",
+ "created_at": "2012-10-22T14:13:35Z",
+ "access_level": 30
+ }
+]
+```
+
+### Add group member
+
+Adds a user to the list of group members.
+
+```
+POST /groups/:id/members
+```
+
+Parameters:
+
+- `id` (required) - The ID or path of a group
+- `user_id` (required) - The ID of a user to add
+- `access_level` (required) - Project access level
+
+### Edit group team member
+
+Updates a group team member to a specified access level.
+
+```
+PUT /groups/:id/members/:user_id
+```
+
+Parameters:
+
+- `id` (required) - The ID of a group
+- `user_id` (required) - The ID of a group member
+- `access_level` (required) - Project access level
+
+### Remove user team member
+
+Removes user from user team.
+
+```
+DELETE /groups/:id/members/:user_id
+```
+
+Parameters:
+
+- `id` (required) - The ID or path of a user group
+- `user_id` (required) - The ID of a group member
+
+## Namespaces in groups
+
+By default, groups only get 20 namespaces at a time because the API results are paginated.
+
+To get more (up to 100), pass the following as an argument to the API call:
+```
+/groups?per_page=100
+```
+
+And to switch pages add:
+```
+/groups?per_page=100&page=2
+```
\ No newline at end of file diff --git a/doc/api/issues.md b/doc/api/issues.md index ceeb683a6bf..d407bc35d79 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -18,6 +18,8 @@ Parameters: - `state` (optional) - Return `all` issues or just those that are `opened` or `closed` - `labels` (optional) - Comma-separated list of label names +- `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` +- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` ```json [ @@ -56,7 +58,7 @@ Parameters: "title": "v1.0", "description": "", "due_date": "2012-07-20", - "state": "reopenend", + "state": "reopened", "updated_at": "2012-07-04T13:42:48Z", "created_at": "2012-07-04T13:42:48Z" }, @@ -97,14 +99,18 @@ GET /projects/:id/issues?labels=foo,bar GET /projects/:id/issues?labels=foo,bar&state=opened GET /projects/:id/issues?milestone=1.0.0 GET /projects/:id/issues?milestone=1.0.0&state=opened +GET /projects/:id/issues?iid=42 ``` Parameters: - `id` (required) - The ID of a project +- `iid` (optional) - Return the issue having the given `iid` - `state` (optional) - Return `all` issues or just those that are `opened` or `closed` - `labels` (optional) - Comma-separated list of label names - `milestone` (optional) - Milestone title +- `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` +- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` ## Single issue @@ -204,7 +210,7 @@ If an error occurs, an error number and a message explaining the reason is retur ## Delete existing issue (**Deprecated**) -The function is deprecated and returns a `405 Method Not Allowed` error if called. An issue gets now closed and is done by calling `PUT /projects/:id/issues/:issue_id` with parameter `closed` set to 1. +The function is deprecated and returns a `405 Method Not Allowed` error if called. An issue gets now closed and is done by calling `PUT /projects/:id/issues/:issue_id` with parameter `state_event` set to `close`. ``` DELETE /projects/:id/issues/:issue_id diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 14884e53915..7b0873a9111 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -2,20 +2,24 @@ ## List merge requests -Get all merge requests for this project. The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`). The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. +Get all merge requests for this project. +The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`). +The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. ``` GET /projects/:id/merge_requests GET /projects/:id/merge_requests?state=opened GET /projects/:id/merge_requests?state=all +GET /projects/:id/merge_requests?iid=42 ``` Parameters: - `id` (required) - The ID of a project +- `iid` (optional) - Return the request having the given `iid` - `state` (optional) - Return `all` requests or just those that are `merged`, `opened` or `closed` -- `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields -- `sort` (optional) - Return requests sorted in `asc` or `desc` order +- `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` +- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` ```json [ @@ -94,6 +98,76 @@ Parameters: } ``` +## Get single MR changes + +Shows information about the merge request including its files and changes + +``` +GET /projects/:id/merge_request/:merge_request_id/changes +``` + +Parameters: + +- `id` (required) - The ID of a project +- `merge_request_id` (required) - The ID of MR + +```json +{ + "id": 21, + "iid": 1, + "project_id": 4, + "title": "Blanditiis beatae suscipit hic assumenda et molestias nisi asperiores repellat et.", + "description": "Qui voluptatibus placeat ipsa alias quasi. Deleniti rem ut sint. Optio velit qui distinctio.", + "state": "reopened", + "created_at": "2015-02-02T19:49:39.159Z", + "updated_at": "2015-02-02T20:08:49.959Z", + "target_branch": "secret_token", + "source_branch": "version-1-9", + "upvotes": 0, + "downvotes": 0, + "author": { + "name": "Chad Hamill", + "username": "jarrett", + "id": 5, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/b95567800f828948baf5f4160ebb2473?s=40&d=identicon" + }, + "assignee": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon" + }, + "source_project_id": 4, + "target_project_id": 4, + "labels": [ ], + "milestone": { + "id": 5, + "iid": 1, + "project_id": 4, + "title": "v2.0", + "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.", + "state": "closed", + "created_at": "2015-02-02T19:49:26.013Z", + "updated_at": "2015-02-02T19:49:26.013Z", + "due_date": null + }, + "files": [ + { + "old_path": "VERSION", + "new_path": "VERSION", + "a_mode": "100644", + "b_mode": "100644", + "diff": "--- a/VERSION\ +++ b/VERSION\ @@ -1 +1 @@\ -1.9.7\ +1.9.8", + "new_file": false, + "renamed_file": false, + "deleted_file": false + } + ] +} +``` + ## Create MR Creates a new merge request. @@ -109,6 +183,7 @@ Parameters: - `target_branch` (required) - The target branch - `assignee_id` (optional) - Assignee user ID - `title` (required) - Title of MR +- `description` (optional) - Description of MR - `target_project_id` (optional) - The target project (numeric id) ```json @@ -146,7 +221,7 @@ If an error occurs, an error number and a message explaining the reason is retur ## Update MR -Updates an existing merge request. You can change branches, title, or even close the MR. +Updates an existing merge request. You can change the target branch, title, or even close the MR. ``` PUT /projects/:id/merge_request/:merge_request_id @@ -156,19 +231,19 @@ Parameters: - `id` (required) - The ID of a project - `merge_request_id` (required) - ID of MR -- `source_branch` - The source branch - `target_branch` - The target branch - `assignee_id` - Assignee user ID - `title` - Title of MR +- `description` - Description of MR - `state_event` - New state (close|reopen|merge) ```json { "id": 1, "target_branch": "master", - "source_branch": "test1", "project_id": 3, "title": "test1", + "description": "description1", "state": "opened", "upvotes": 0, "downvotes": 0, @@ -300,7 +375,7 @@ Parameters: } }, { - "note": "_Status changed to closed_", + "note": "Status changed to closed", "author": { "id": 11, "username": "admin", @@ -313,6 +388,6 @@ Parameters: ] ``` -## Comments on issues +## Comments on merge requets Comments are done via the notes resource. diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 2f525327504..a6828728264 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -6,6 +6,7 @@ Returns a list of project milestones. ``` GET /projects/:id/milestones +GET /projects/:id/milestones?iid=42 ``` ```json @@ -27,6 +28,7 @@ GET /projects/:id/milestones Parameters: - `id` (required) - The ID of a project +- `iid` (optional) - Return the milestone having the given `iid` ## Get single milestone @@ -72,3 +74,16 @@ Parameters: - `description` (optional) - The description of a milestone - `due_date` (optional) - The due date of the milestone - `state_event` (optional) - The state event of the milestone (close|activate) + +## Get all issues assigned to a single milestone + +Gets all issues assigned to a single project milestone. + +``` +GET /projects/:id/milestones/:milestone_id/issues +``` + +Parameters: + +- `id` (required) - The ID of a project +- `milestone_id` (required) - The ID of a project milestone diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md new file mode 100644 index 00000000000..7b3238441f6 --- /dev/null +++ b/doc/api/namespaces.md @@ -0,0 +1,44 @@ +# Namespaces + +## List namespaces + +Get a list of namespaces. (As user: my namespaces, as admin: all namespaces) + +``` +GET /namespaces +``` + +```json +[ + { + "id": 1, + "path": "user1", + "kind": "user" + }, + { + "id": 2, + "path": "group1", + "kind": "group" + } +] +``` + +You can search for namespaces by name or path, see below. + +## Search for namespace + +Get all namespaces that match your string in their name or path. + +``` +GET /namespaces?search=foobar +``` + +```json +[ + { + "id": 1, + "path": "user1", + "kind": "user" + } +] +``` diff --git a/doc/api/notes.md b/doc/api/notes.md index b5256ac803e..ee2f9fa0eac 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -21,7 +21,7 @@ Parameters: [ { "id": 302, - "body": "_Status changed to closed_", + "body": "Status changed to closed", "attachment": null, "author": { "id": 1, @@ -78,6 +78,21 @@ Parameters: - `issue_id` (required) - The ID of an issue - `body` (required) - The content of a note +### Modify existing issue note + +Modify existing note of an issue. + +``` +PUT /projects/:id/issues/:issue_id/notes/:note_id +``` + +Parameters: + +- `id` (required) - The ID of a project +- `issue_id` (required) - The ID of an issue +- `note_id` (required) - The ID of a note +- `body` (required) - The content of a note + ## Snippets ### List all snippet notes @@ -137,7 +152,22 @@ POST /projects/:id/snippets/:snippet_id/notes Parameters: - `id` (required) - The ID of a project -- `snippet_id` (required) - The ID of an snippet +- `snippet_id` (required) - The ID of a snippet +- `body` (required) - The content of a note + +### Modify existing snippet note + +Modify existing note of a snippet. + +``` +PUT /projects/:id/snippets/:snippet_id/notes/:note_id +``` + +Parameters: + +- `id` (required) - The ID of a project +- `snippet_id` (required) - The ID of a snippet +- `note_id` (required) - The ID of a note - `body` (required) - The content of a note ## Merge Requests @@ -199,3 +229,18 @@ Parameters: - `id` (required) - The ID of a project - `merge_request_id` (required) - The ID of a merge request - `body` (required) - The content of a note + +### Modify existing merge request note + +Modify existing note of a merge request. + +``` +PUT /projects/:id/merge_requests/:merge_request_id/notes/:note_id +``` + +Parameters: + +- `id` (required) - The ID of a project +- `merge_request_id` (required) - The ID of a merge request +- `note_id` (required) - The ID of a note +- `body` (required) - The content of a note diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md new file mode 100644 index 00000000000..d416a826f79 --- /dev/null +++ b/doc/api/oauth2.md @@ -0,0 +1,102 @@ +# GitLab as an OAuth2 client + +This document is about using other OAuth authentication service providers to sign into GitLab. +If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md). + +OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password. + +Before using the OAuth2 you should create an application in user's account. Each application gets a unique App ID and App Secret parameters. You should not share these. + +This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper) + +## Web Application Flow + +This flow is using for authentication from third-party web sites and is probably used the most. +It basically consists of an exchange of an authorization token for an access token. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1) + +This flow consists from 3 steps. + +### 1. Registering the client + +Create an application in user's account profile. + +### 2. Requesting authorization + +To request the authorization token, you should visit the `/oauth/authorize` endpoint. You can do that by visiting manually the URL: + +``` +http://localhost:3000/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code +``` + +Where REDIRECT_URI is the URL in your app where users will be sent after authorization. + +### 3. Requesting the access token + +To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. In this case, I used rest-client: + +``` +parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=AUTHORIZATION_CODE&redirect_uri=REDIRECT_URI' +RestClient.post 'http://localhost:3000/oauth/token', parameters + +# The response will be +{ + "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", + "token_type": "bearer", + "expires_in": 7200, + "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" +} +``` + +You can now make requests to the API with the access token returned. + +### Use the access token to access the API + +The access token allows you to make requests to the API on a behalf of a user. + +``` +GET https://localhost:3000/api/v3/user?access_token=OAUTH-TOKEN +``` + +Or you can put the token to the Authorization header: + +``` +curl -H "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user +``` + +## Resource Owner Password Credentials + +In this flow, a token is requested in exchange for the resource owner credentials (username and password). +The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the +client is part of the device operating system or a highly privileged application), and when other authorization grant types are not +available (such as an authorization code). + +Even though this grant type requires direct client access to the resource owner credentials, the resource owner credentials are used +for a single request and are exchanged for an access token. This grant type can eliminate the need for the client to store the +resource owner credentials for future use, by exchanging the credentials with a long-lived access token or refresh token. +You can do POST request to `/oauth/token` with parameters: + +``` +{ + "grant_type" : "password", + "username" : "user@example.com", + "password" : "sekret" +} +``` + +Then, you'll receive the access token back in the response: + +``` +{ + "access_token": "1f0af717251950dbd4d73154fdf0a474a5c5119adad999683f5b450c460726aa", + "token_type": "bearer", + "expires_in": 7200 +} +``` + +For testing you can use the oauth2 ruby gem: + +``` +client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http://example.com") +access_token = client.password.get_token('user@example.com', 'sekret') +puts access_token.token +``` diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index 50e134847c0..a7acf37b5bc 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -1,5 +1,18 @@ # Project snippets +### Snippet visibility level + +Snippets in GitLab can be either private, internal or public. +You can set it with the `visibility_level` field in the snippet. + +Constants for snippet visibility levels are: + +| Visibility | visibility_level | Description | +| ---------- | ---------------- | ----------- | +| Private | `0` | The snippet is visible only the snippet creator | +| Internal | `10` | The snippet is visible for any logged in user | +| Public | `20` | The snippet can be accessed without any authentication | + ## List snippets Get a list of project snippets. @@ -58,6 +71,7 @@ Parameters: - `title` (required) - The title of a snippet - `file_name` (required) - The name of a snippet file - `code` (required) - The content of a snippet +- `visibility_level` (required) - The snippet's visibility ## Update snippet @@ -74,6 +88,7 @@ Parameters: - `title` (optional) - The title of a snippet - `file_name` (optional) - The name of a snippet file - `code` (optional) - The content of a snippet +- `visibility_level` (optional) - The snippet's visibility ## Delete snippet diff --git a/doc/api/projects.md b/doc/api/projects.md index 0055e2e476f..17c014019ea 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1,5 +1,23 @@ # Projects + +### Project visibility level + +Project in GitLab has be either private, internal or public. +You can determine it by `visibility_level` field in project. + +Constants for project visibility levels are next: + +* Private. `visibility_level` is `0`. + Project access must be granted explicitly for each user. + +* Internal. `visibility_level` is `10`. + The project can be cloned by any logged in user. + +* Public. `visibility_level` is `20`. + The project can be cloned without any authentication. + + ## List projects Get a list of projects accessible by the authenticated user. @@ -11,6 +29,10 @@ GET /projects Parameters: - `archived` (optional) - if passed, limit by archived status +- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` +- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` +- `search` (optional) - Return list of authorized projects according to a search criteria +- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first ```json [ @@ -23,6 +45,10 @@ Parameters: "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git", "web_url": "http://example.com/diaspora/diaspora-client", + "tag_list": [ + "example", + "disapora client" + ], "owner": { "id": 3, "name": "Diaspora", @@ -38,6 +64,7 @@ Parameters: "snippets_enabled": false, "created_at": "2013-09-30T13: 46: 02Z", "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, "namespace": { "created_at": "2013-09-30T13: 46: 02Z", "description": "", @@ -47,7 +74,8 @@ Parameters: "path": "diaspora", "updated_at": "2013-09-30T13: 46: 02Z" }, - "archived": false + "archived": false, + "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png" }, { "id": 6, @@ -58,6 +86,10 @@ Parameters: "ssh_url_to_repo": "git@example.com:brightbox/puppet.git", "http_url_to_repo": "http://example.com/brightbox/puppet.git", "web_url": "http://example.com/brightbox/puppet", + "tag_list": [ + "example", + "puppet" + ], "owner": { "id": 4, "name": "Brightbox", @@ -73,6 +105,7 @@ Parameters: "snippets_enabled": false, "created_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z", + "creator_id": 3, "namespace": { "created_at": "2013-09-30T13:46:02Z", "description": "", @@ -82,7 +115,8 @@ Parameters: "path": "brightbox", "updated_at": "2013-09-30T13:46:02Z" }, - "archived": false + "archived": false, + "avatar_url": null } ] ``` @@ -95,6 +129,14 @@ Get a list of projects which are owned by the authenticated user. GET /projects/owned ``` +Parameters: + +- `archived` (optional) - if passed, limit by archived status +- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` +- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` +- `search` (optional) - Return list of authorized projects according to a search criteria +- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first + ### List ALL projects Get a list of all GitLab projects (admin only). @@ -103,6 +145,14 @@ Get a list of all GitLab projects (admin only). GET /projects/all ``` +Parameters: + +- `archived` (optional) - if passed, limit by archived status +- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` +- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` +- `search` (optional) - Return list of authorized projects according to a search criteria +- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first + ### Get single project Get a specific project, identified by project ID or NAMESPACE/PROJECT_NAME, which is owned by the authenticated user. @@ -126,6 +176,10 @@ Parameters: "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], "owner": { "id": 3, "name": "Diaspora", @@ -141,6 +195,7 @@ Parameters: "snippets_enabled": false, "created_at": "2013-09-30T13: 46: 02Z", "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, "namespace": { "created_at": "2013-09-30T13: 46: 02Z", "description": "", @@ -160,7 +215,8 @@ Parameters: "notification_level": 3 } }, - "archived": false + "archived": false, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png" } ``` @@ -284,6 +340,31 @@ Parameters: - `visibility_level` (optional) - `import_url` (optional) +### Edit project + +Updates an existing project + +``` +PUT /projects/:id +``` + +Parameters: + +- `id` (required) - The ID of a project +- `name` (optional) - project name +- `path` (optional) - repository name for project +- `description` (optional) - short project description +- `default_branch` (optional) +- `issues_enabled` (optional) +- `merge_requests_enabled` (optional) +- `wiki_enabled` (optional) +- `snippets_enabled` (optional) +- `public` (optional) - if `true` same as setting visibility_level = 20 +- `visibility_level` (optional) + +On success, method returns 200 with the updated project. If parameters are +invalid, 400 is returned. + ### Fork project Forks a project into the user namespace of the authenticated user. @@ -513,7 +594,7 @@ Parameters: } ], "tree": "c68537c6534a02cc2b176ca1549f4ffa190b58ee", - "message": "give caolan credit where it's due (up top)", + "message": "give Caolan credit where it's due (up top)", "author": { "name": "Jeremy Ashkenas", "email": "jashkenas@example.com" @@ -628,6 +709,8 @@ GET /projects/search/:query Parameters: -- query (required) - A string contained in the project name -- per_page (optional) - number of projects to return per page -- page (optional) - the page to retrieve +- `query` (required) - A string contained in the project name +- `per_page` (optional) - number of projects to return per page +- `page` (optional) - the page to retrieve +- `order_by` (optional) - Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields +- `sort` (optional) - Return requests sorted in `asc` or `desc` order diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 8acf85d21c8..33167453802 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -15,24 +15,21 @@ Parameters: ```json [ { - "name": "v1.0.0", "commit": { + "author_name": "John Smith", + "author_email": "john@example.com", + "authored_date": "2012-05-28T04:42:42-07:00", + "committed_date": "2012-05-28T04:42:42-07:00", + "committer_name": "Jack Smith", + "committer_email": "jack@example.com", "id": "2695effb5807a22ff3d138d593fd856244e155e7", - "parents": [], - "tree": "38017f2f189336fe4497e9d230c5bb1bf873f08d", "message": "Initial commit", - "author": { - "name": "John Smith", - "email": "john@example.com" - }, - "committer": { - "name": "Jack Smith", - "email": "jack@example.com" - }, - "authored_date": "2012-05-28T04:42:42-07:00", - "committed_date": "2012-05-28T04:42:42-07:00" + "parents_ids": [ + "2a4b78934375d7f53875269ffd4f45fd83a84ebe" + ] }, - "protected": null + "name": "v1.0.0", + "message": null } ] ``` @@ -53,23 +50,23 @@ Parameters: - `message` (optional) - Creates annotated tag. ```json -[ - { - "name": "v1.0.0", - "message": "Release 1.0.0", - "commit": { - "id": "2695effb5807a22ff3d138d593fd856244e155e7", - "parents": [], - "message": "Initial commit", - "authored_date": "2012-05-28T04:42:42-07:00", - "author_name": "John Smith", - "author email": "john@example.com", - "committer_name": "Jack Smith", - "committed_date": "2012-05-28T04:42:42-07:00", - "committer_email": "jack@example.com" - }, - } -] +{ + "commit": { + "author_name": "John Smith", + "author_email": "john@example.com", + "authored_date": "2012-05-28T04:42:42-07:00", + "committed_date": "2012-05-28T04:42:42-07:00", + "committer_name": "Jack Smith", + "committer_email": "jack@example.com", + "id": "2695effb5807a22ff3d138d593fd856244e155e7", + "message": "Initial commit", + "parents_ids": [ + "2a4b78934375d7f53875269ffd4f45fd83a84ebe" + ] + }, + "name": "v1.0.0", + "message": null +} ``` The message will be `nil` when creating a lightweight tag otherwise it will contain the annotation. diff --git a/doc/api/services.md b/doc/api/services.md index ab9f9c00c67..cbf767d1b25 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -13,7 +13,7 @@ PUT /projects/:id/services/gitlab-ci Parameters: - `token` (required) - CI project token -- `project_url` (required) - CI project url +- `project_url` (required) - CI project URL ### Delete GitLab CI service @@ -23,23 +23,23 @@ Delete GitLab CI service settings for a project. DELETE /projects/:id/services/gitlab-ci ``` -## Hipchat +## HipChat -### Edit Hipchat service +### Edit HipChat service -Set Hipchat service for project. +Set HipChat service for project. ``` PUT /projects/:id/services/hipchat ``` Parameters: -- `token` (required) - Hipchat token -- `room` (required) - Hipchat room name +- `token` (required) - HipChat token +- `room` (required) - HipChat room name -### Delete Hipchat service +### Delete HipChat service -Delete Hipchat service for a project. +Delete HipChat service for a project. ``` DELETE /projects/:id/services/hipchat diff --git a/doc/api/users.md b/doc/api/users.md index b30a31deccc..cd141daadc8 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -57,7 +57,8 @@ GET /users "color_scheme_id": 2, "is_admin": false, "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", - "can_create_group": true + "can_create_group": true, + "current_sign_in_at": "2014-03-19T13:12:15Z" }, { "id": 2, @@ -79,7 +80,8 @@ GET /users "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", "can_create_group": true, "can_create_project": true, - "projects_limit": 100 + "projects_limit": 100, + "current_sign_in_at": "2014-03-19T17:54:13Z" } ] ``` @@ -170,6 +172,7 @@ Parameters: - `bio` (optional) - User's biography - `admin` (optional) - User is admin - true or false (default) - `can_create_group` (optional) - User can create groups - true or false +- `confirm` (optional) - Require confirmation - true (default) or false ## User modification @@ -322,6 +325,31 @@ Parameters: - `title` (required) - new SSH Key's title - `key` (required) - new SSH key +```json +{ + "created_at": "2015-01-21T17:44:33.512Z", + "key": "ssh-dss AAAAB3NzaC1kc3MAAACBAMLrhYgI3atfrSD6KDas1b/3n6R/HP+bLaHHX6oh+L1vg31mdUqK0Ac/NjZoQunavoyzqdPYhFz9zzOezCrZKjuJDS3NRK9rspvjgM0xYR4d47oNZbdZbwkI4cTv/gcMlquRy0OvpfIvJtjtaJWMwTLtM5VhRusRuUlpH99UUVeXAAAAFQCVyX+92hBEjInEKL0v13c/egDCTQAAAIEAvFdWGq0ccOPbw4f/F8LpZqvWDydAcpXHV3thwb7WkFfppvm4SZte0zds1FJ+Hr8Xzzc5zMHe6J4Nlay/rP4ewmIW7iFKNBEYb/yWa+ceLrs+TfR672TaAgO6o7iSRofEq5YLdwgrwkMmIawa21FrZ2D9SPao/IwvENzk/xcHu7YAAACAQFXQH6HQnxOrw4dqf0NqeKy1tfIPxYYUZhPJfo9O0AmBW2S36pD2l14kS89fvz6Y1g8gN/FwFnRncMzlLY/hX70FSc/3hKBSbH6C6j8hwlgFKfizav21eS358JJz93leOakJZnGb8XlWvz1UJbwCsnR2VEY8Dz90uIk1l/UqHkA= loic@call", + "title": "ABC", + "id": 4 +} +``` + +Will return created key with status `201 Created` on success. If an +error occurs a `400 Bad Request` is returned with a message explaining the error: + +```json +{ + "message": { + "fingerprint": [ + "has already been taken" + ], + "key": [ + "has already been taken" + ] + } +} +``` + ## Add SSH key for user Create new key owned by specified user. Available only for admin diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md index ddc0c8eac2b..64f128f5a63 100644 --- a/doc/customization/issue_closing.md +++ b/doc/customization/issue_closing.md @@ -1,5 +1,38 @@ # Issue closing pattern -By default you can close issues from commit messages by saying 'Closes #12' or 'Fixed #101'. +Here's how to close multiple issues in one commit message: -If you want to customize the message please do so in [gitlab.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/73b92f85bcd6c213b845cc997843a969cf0906cf/config/gitlab.yml.example#L73) +If a commit message matches the regular expression below, all issues referenced from +the matched text will be closed. This happens when the commit is pushed or merged +into the default branch of a project. + +When not specified, the default issue_closing_pattern as shown below will be used: + +```bash +((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+) +``` + +For example: + +``` +git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes #22). This commit is also related to #17 and fixes #18, #19 and #23." +``` + +will close `#20`, `#21`, `#22`, `#18`, `#19` and `#23`, but `#17` won't be closed +as it does not match the pattern. It also works with multiline commit messages. + +Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site +to test your own patterns. + +## Change the pattern + +For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`: + +``` +issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' +``` + +For manual installs you can customize the pattern in [gitlab.yml][0]. + +[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/40c3675372320febf5264061c9bcd63db2dfd13c/config/gitlab.yml.example#L65 +[1]: http://rubular.com/r/Xmbexed1OJ
\ No newline at end of file diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md index 4dffd3027a9..ee57fdc6590 100644 --- a/doc/customization/libravatar.md +++ b/doc/customization/libravatar.md @@ -16,7 +16,7 @@ the configuration options as follows: ```yml gravatar: enabled: true - # gravatar urls: possible placeholders: %{hash} %{size} %{email} + # gravatar URLs: possible placeholders: %{hash} %{size} %{email} plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" ``` @@ -25,14 +25,14 @@ the configuration options as follows: ```yml gravatar: enabled: true - # gravatar urls: possible placeholders: %{hash} %{size} %{email} + # gravatar URLs: possible placeholders: %{hash} %{size} %{email} ssl_url: "https://seccdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" ``` ## Self-hosted -If you are [running your own libravatar service](http://wiki.libravatar.org/running_your_own/) the url will be different in the configuration -but the important part is to provide the same placeholders so GitLab can parse the url correctly. +If you are [running your own libravatar service](http://wiki.libravatar.org/running_your_own/) the URL will be different in the configuration +but the important part is to provide the same placeholders so GitLab can parse the URL correctly. For example, you host a service on `http://libravatar.example.com` the `plain_url` you need to supply in `gitlab.yml` is @@ -65,5 +65,5 @@ Run `sudo gitlab-ctl reconfigure` for changes to take effect. [Libravatar supports different sets](http://wiki.libravatar.org/api/) of `missing images` for emails not found on the Libravatar service. -In order to use a different set other than `identicon`, replace `&d=identicon` portion of the url with another supported set. -For example, you can use `retro` set in which case url would look like: `plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=retro"` +In order to use a different set other than `identicon`, replace `&d=identicon` portion of the URL with another supported set. +For example, you can use `retro` set in which case the URL would look like: `plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=retro"` diff --git a/doc/development/README.md b/doc/development/README.md index c31e5d7ae97..6bc8e1888db 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -1,7 +1,10 @@ -# Development +# Development - [Architecture](architecture.md) of GitLab - [Shell commands](shell_commands.md) in the GitLab codebase - [Rake tasks](rake_tasks.md) for development - [CI setup](ci_setup.md) for testing GitLab - [Sidekiq debugging](sidekiq_debugging.md) +- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements +- [Migration Style Guide](migration_style_guide.md) for creating safe migrations +- [How to dump production data to staging](dump_db.md) diff --git a/doc/development/architecture.md b/doc/development/architecture.md index 209182e7742..541af487bb1 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -16,8 +16,8 @@ You can imagine GitLab as a physical office. They can be stored in a warehouse. This can be either a hard disk, or something more complex, such as a NFS filesystem; -**NginX** acts like the front-desk. -Users come to NginX and request actions to be done by workers in the office; +**Nginx** acts like the front-desk. +Users come to Nginx and request actions to be done by workers in the office; **The database** is a series of metal file cabinets with information on: - The goods in the warehouse (metadata, issues, merge requests etc); @@ -54,7 +54,7 @@ To serve repositories over SSH there's an add-on application called gitlab-shell  -A typical install of GitLab will be on Ubuntu Linux or RHEL/CentOS. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs. +A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs. The GitLab web app uses MySQL or PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository. `/home/git/gitlab-satellites` keeps checked out repositories when performing actions such as a merge request, editing files in the web interface, etc. @@ -70,7 +70,7 @@ To summarize here's the [directory structure of the `git` user home directory](. ps aux | grep '^git' -GitLab has several components to operate. As a system user (i.e. any user that is not the `git` user) it requires a persistent database (MySQL/PostreSQL) and redis database. It also uses Apache httpd or nginx to proxypass Unicorn. As the `git` user it starts Sidekiq and Unicorn (a simple ruby HTTP server running on port `8080` by default). Under the GitLab user there are normally 4 processes: `unicorn_rails master` (1 process), `unicorn_rails worker` (2 processes), `sidekiq` (1 process). +GitLab has several components to operate. As a system user (i.e. any user that is not the `git` user) it requires a persistent database (MySQL/PostreSQL) and redis database. It also uses Apache httpd or Nginx to proxypass Unicorn. As the `git` user it starts Sidekiq and Unicorn (a simple ruby HTTP server running on port `8080` by default). Under the GitLab user there are normally 4 processes: `unicorn_rails master` (1 process), `unicorn_rails worker` (2 processes), `sidekiq` (1 process). ### Repository access @@ -146,13 +146,13 @@ nginx Apache httpd -- [Explanation of apache logs](http://httpd.apache.org/docs/2.2/logs.html). +- [Explanation of Apache logs](http://httpd.apache.org/docs/2.2/logs.html). - `/var/log/apache2/` contains error and output logs (on Ubuntu). - `/var/log/httpd/` contains error and output logs (on RHEL). redis -- `/var/log/redis/redis.log` there are also logrotated logs there. +- `/var/log/redis/redis.log` there are also log-rotated logs there. PostgreSQL diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md index ee16aedafe7..f9b48868182 100644 --- a/doc/development/ci_setup.md +++ b/doc/development/ci_setup.md @@ -26,7 +26,7 @@ We use [these build scripts](https://gitlab.com/gitlab-org/gitlab-ci/blob/master # Build configuration on [Semaphore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for testing the [GitHub.com repo](https://github.com/gitlabhq/gitlabhq) - Language: Ruby -- Ruby verion: 2.1.2 +- Ruby version: 2.1.2 - database.yml: pg Build commands @@ -37,7 +37,10 @@ bundle install --deployment --path vendor/bundle (Setup) cp config/gitlab.yml.example config/gitlab.yml (Setup) bundle exec rake db:create (Setup) bundle exec rake spinach (Thread #1) -bundle exec rake spec (Thread #2) +bundle exec rake spec (thread #2) +bundle exec rake rubocop (thread #3) +bundle exec rake brakeman (thread #4) +bundle exec rake jasmine:ci (thread #5) ``` Use rubygems mirror. diff --git a/doc/development/db_dump.md b/doc/development/db_dump.md new file mode 100644 index 00000000000..21f1b3edecd --- /dev/null +++ b/doc/development/db_dump.md @@ -0,0 +1,50 @@ +# Importing a database dump into a staging enviroment + +Sometimes it is useful to import the database from a production environment +into a staging environment for testing. The procedure below assumes you have +SSH+sudo access to both the production environment and the staging VM. + +**Destroy your staging VM** when you are done with it. It is important to avoid +data leaks. + +On the staging VM, add the following line to `/etc/gitlab/gitlab.rb` to speed up +large database imports. + +``` +# On STAGING +echo "postgresql['checkpoint_segments'] = 64" | sudo tee -a /etc/gitlab/gitlab.rb +sudo touch /etc/gitlab/skip-auto-migrations +sudo gitlab-ctl reconfigure +sudo gitlab-ctl stop unicorn +sudo gitlab-ctl stop sidekiq +``` + +Next, we let the production environment stream a compressed SQL dump to our +local machine via SSH, and redirect this stream to a psql client on the staging +VM. + +``` +# On LOCAL MACHINE +ssh -C gitlab.example.com sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -Cc gitlabhq_production |\ + ssh -C staging-vm sudo -u gitlab-psql /opt/gitlab/embedded/bin/psql -d template1 +``` + +## Recreating directory structure + +If you need to re-create some directory structure on the staging server you can +use this procedure. + +First, on the production server, create a list of directories you want to +re-create. + +``` +# On PRODUCTION +(umask 077; sudo find /var/opt/gitlab/git-data/repositories -maxdepth 1 -type d -print0 > directories.txt) +``` + +Copy `directories.txt` to the staging server and create the directories there. + +``` +# On STAGING +sudo -u git xargs -0 mkdir -p < directories.txt +``` diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md new file mode 100644 index 00000000000..4fa1961fde9 --- /dev/null +++ b/doc/development/migration_style_guide.md @@ -0,0 +1,88 @@ +# Migration Style Guide + +When writing migrations for GitLab, you have to take into account that +these will be ran by hundreds of thousands of organizations of all sizes, some with +many years of data in their database. + +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. + +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 +about the state of the database. + +Please don't depend on GitLab specific code since it can change in future versions. +If needed copy-paste GitLab code into the migration to make make it forward compatible. + +## Comments in the migration + +Each migration you write needs to have the two following pieces of information +as comments. + +### Online, Offline, errors? + +First, you need to provide information on whether the migration can be applied: + +1. online without errors (works on previous version and new one) +2. online with errors on old instances after migrating +3. online with errors on new instances while migrating +4. offline (needs to happen without app servers to prevent db corruption) + +It is always preferable to have a migration run online. If you expect the migration +to take particularly long (for instance, if it loops through all notes), +this is valuable information to add. + +### Reversibility + +Your migration should be reversible. This is very important, as it should +be possible to downgrade in case of a vulnerability or bugs. + +In your migration, add a comment describing how the reversibility of the +migration was tested. + + +## Removing indices + +If you need to remove index, please add a condition like in following example: + +``` +remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) +``` + +## Adding indices + +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. + +## Testing + +Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct. + +Make sure your migration can be reversed. + +## Data migration + +Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of using plain SQL you need to quote all input manually with `quote_string` helper. + +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 +``` + +Example with plain SQL and `quote_string` helper: + +``` +select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag| + tag_name = quote_string(tag["name"]) + duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]} + origin_tag_id = duplicate_ids.first + duplicate_ids.delete origin_tag_id + + 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 +``` diff --git a/doc/development/omnibus.md b/doc/development/omnibus.md new file mode 100644 index 00000000000..0ba354d28a2 --- /dev/null +++ b/doc/development/omnibus.md @@ -0,0 +1,32 @@ +# What you should know about omnibus packages + +Most users install GitLab using our omnibus packages. As a developer it can be +good to know how the omnibus packages differ from what you have on your laptop +when you are coding. + +## Files are owned by root by default + +All the files in the Rails tree (`app/`, `config/` etc.) are owned by 'root' in +omnibus installations. This makes the installation simpler and it provides +extra security. The omnibus reconfigure script contains commands that give +write access to the 'git' user only where needed. + +For example, the 'git' user is allowed to write in the `log/` directory, in +`public/uploads`, and they are allowed to rewrite the `db/schema.rb` file. + +In other cases, the reconfigure script tricks GitLab into not trying to write a +file. For instance, GitLab will generate a `.secret` file if it cannot find one +and write it to the Rails root. In the omnibus packages, reconfigure writes the +`.secret` file first, so that GitLab never tries to write it. + +## Code, data and logs are in separate directories + +The omnibus design separates code (read-only, under `/opt/gitlab`) from data +(read/write, under `/var/opt/gitlab`) and logs (read/write, under +`/var/log/gitlab`). To make this happen the reconfigure script sets custom +paths where it can in GitLab config files, and where there are no path +settings, it uses symlinks. + +For example, `config/gitlab.yml` is treated as data so that file is a symlink. +The same goes for `public/uploads`. The `log/` directory is replaced by omnibus +with a symlink to `/var/log/gitlab/gitlab-rails`. diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index 6d9ac161e91..53f8095cb13 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -1,6 +1,6 @@ # Rake tasks for developers -## Setup db with developer seeds: +## Setup db with developer seeds Note that if your db user does not have advanced privileges you must create the db manually before running this command. @@ -8,6 +8,10 @@ Note that if your db user does not have advanced privileges you must create the bundle exec rake setup ``` +The `setup` task is a alias for `gitlab:setup`. +This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and fianlly it calls `db:seed_fu` to seed the database. +Note: `db:setup` calls `db:seed` but this does nothing. + ## Run tests This runs all test suites present in GitLab. diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md index 23c8365c340..2d1d0fb4154 100644 --- a/doc/development/shell_commands.md +++ b/doc/development/shell_commands.md @@ -1,5 +1,8 @@ # Guidelines for shell commands in the GitLab codebase +This document contains guidelines for working with processes and files in the GitLab codebase. +These guidelines are meant to make your code more reliable _and_ secure. + ## References - [Google Ruby Security Reviewer's Guide](https://code.google.com/p/ruby-security/wiki/Guide) @@ -105,7 +108,102 @@ In other repositories, such as gitlab-shell you can also use `IO.popen`. ```ruby # Safe IO.popen example -logs = IO.popen(%W(git log), chdir: repo_dir).read +logs = IO.popen(%W(git log), chdir: repo_dir) { |p| p.read } ``` Note that unlike `Gitlab::Popen.popen`, `IO.popen` does not capture standard error. + +## Avoid user input at the start of path strings + +Various methods for opening and reading files in Ruby can be used to read the +standard output of a process instead of a file. The following two commands do +roughly the same: + +``` +`touch /tmp/pawned-by-backticks` +File.read('|touch /tmp/pawned-by-file-read') +``` + +The key is to open a 'file' whose name starts with a `|`. +Affected methods include Kernel#open, File::read, File::open, IO::open and IO::read. + +You can protect against this behavior of 'open' and 'read' by ensuring that an +attacker cannot control the start of the filename string you are opening. For +instance, the following is sufficient to protect against accidentally starting +a shell command with `|`: + +``` +# we assume repo_path is not controlled by the attacker (user) +path = File.join(repo_path, user_input) +# path cannot start with '|' now. +File.read(path) +``` + +If you have to use user input a relative path, prefix `./` to the path. + +Prefixing user-supplied paths also offers extra protection against paths +starting with `-` (see the discussion about using `--` above). + +## Guard against path traversal + +Path traversal is a security where the program (GitLab) tries to restrict user +access to a certain directory on disk, but the user manages to open a file +outside that directory by taking advantage of the `../` path notation. + +``` +# Suppose the user gave us a path and they are trying to trick us +user_input = '../other-repo.git/other-file' + +# We look up the repo path somewhere +repo_path = 'repositories/user-repo.git' + +# The intention of the code below is to open a file under repo_path, but +# because the user used '..' she can 'break out' into +# 'repositories/other-repo.git' +full_path = File.join(repo_path, user_input) +File.open(full_path) do # Oops! +``` + +A good way to protect against this is to compare the full path with its +'absolute path' according to Ruby's `File.absolute_path`. + +``` +full_path = File.join(repo_path, user_input) +if full_path != File.absolute_path(full_path) + raise "Invalid path: #{full_path.inspect}" +end + +File.open(full_path) do # Etc. +``` + +A check like this could have avoided CVE-2013-4583. + +## Properly anchor regular expressions to the start and end of strings + +When using regular expressions to validate user input that is passed as an argument to a shell command, make sure to use the `\A` and `\z` anchors that designate the start and end of the string, rather than `^` and `$`, or no anchors at all. + +If you don't, an attacker could use this to execute commands with potentially harmful effect. + +For example, when a project's `import_url` is validated like below, the user could trick GitLab into cloning from a Git repository on the local filesystem. + +```ruby +validates :import_url, format: { with: URI.regexp(%w(ssh git http https)) } +# URI.regexp(%w(ssh git http https)) roughly evaluates to /(ssh|git|http|https):(something_that_looks_like_a_url)/ +``` + +Suppose the user submits the following as their import URL: + +``` +file://git:/tmp/lol +``` + +Since there are no anchors in the used regular expression, the `git:/tmp/lol` in the value would match, and the validation would pass. + +When importing, GitLab would execute the following command, passing the `import_url` as an argument: + + +```sh +git clone file://git:/tmp/lol +``` + +Git would simply ignore the `git:` part, interpret the path as `file:///tmp/lol` and import the repository into the new project, in turn potentially giving the attacker access to any repository in the system, whether private or not. diff --git a/doc/sidekiq_debugging.md b/doc/development/sidekiq_debugging.md index cea11e5f126..cea11e5f126 100644 --- a/doc/sidekiq_debugging.md +++ b/doc/development/sidekiq_debugging.md diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md new file mode 100644 index 00000000000..2f01defc11d --- /dev/null +++ b/doc/development/ui_guide.md @@ -0,0 +1,12 @@ +# UI Guide for building GitLab + +## Best practices for creating new pages in GitLab + +TODO: write some best practices when develop GitLab features. + +## GitLab UI development kit + +We created a page inside GitLab where you can check commonly used html and css elements. + +When you run GitLab instance locally - just visit http://localhost:3000/help/ui page to see UI examples +you can use during GitLab development. diff --git a/doc/gitlab_basics/README.md b/doc/gitlab_basics/README.md new file mode 100644 index 00000000000..c434e0146e3 --- /dev/null +++ b/doc/gitlab_basics/README.md @@ -0,0 +1,7 @@ +# GitLab basics + +Step-by-step guides on the basics of working with Git and GitLab. + +* [Start using Git on the commandline](start_using_git.md) + + diff --git a/doc/gitlab_basics/basicsimages/add_new_merge_request.png b/doc/gitlab_basics/basicsimages/add_new_merge_request.png Binary files differnew file mode 100644 index 00000000000..9d93b217a59 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/add_new_merge_request.png diff --git a/doc/gitlab_basics/basicsimages/add_sshkey.png b/doc/gitlab_basics/basicsimages/add_sshkey.png Binary files differnew file mode 100644 index 00000000000..2dede97aa40 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/add_sshkey.png diff --git a/doc/gitlab_basics/basicsimages/branch_info.png b/doc/gitlab_basics/basicsimages/branch_info.png Binary files differnew file mode 100644 index 00000000000..c5e38b552a5 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/branch_info.png diff --git a/doc/gitlab_basics/basicsimages/branch_name.png b/doc/gitlab_basics/basicsimages/branch_name.png Binary files differnew file mode 100644 index 00000000000..06e77f5eea9 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/branch_name.png diff --git a/doc/gitlab_basics/basicsimages/branches.png b/doc/gitlab_basics/basicsimages/branches.png Binary files differnew file mode 100644 index 00000000000..c18fa83b968 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/branches.png diff --git a/doc/gitlab_basics/basicsimages/commit_changes.png b/doc/gitlab_basics/basicsimages/commit_changes.png Binary files differnew file mode 100644 index 00000000000..81588336f37 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/commit_changes.png diff --git a/doc/gitlab_basics/basicsimages/commit_message.png b/doc/gitlab_basics/basicsimages/commit_message.png Binary files differnew file mode 100644 index 00000000000..0df2c32653c --- /dev/null +++ b/doc/gitlab_basics/basicsimages/commit_message.png diff --git a/doc/gitlab_basics/basicsimages/commits.png b/doc/gitlab_basics/basicsimages/commits.png Binary files differnew file mode 100644 index 00000000000..7e606539077 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/commits.png diff --git a/doc/gitlab_basics/basicsimages/compare_braches.png b/doc/gitlab_basics/basicsimages/compare_braches.png Binary files differnew file mode 100644 index 00000000000..7eebaed9075 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/compare_braches.png diff --git a/doc/gitlab_basics/basicsimages/create_file.png b/doc/gitlab_basics/basicsimages/create_file.png Binary files differnew file mode 100644 index 00000000000..688e355cca2 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/create_file.png diff --git a/doc/gitlab_basics/basicsimages/create_group.png b/doc/gitlab_basics/basicsimages/create_group.png Binary files differnew file mode 100644 index 00000000000..57da898abdc --- /dev/null +++ b/doc/gitlab_basics/basicsimages/create_group.png diff --git a/doc/gitlab_basics/basicsimages/edit_file.png b/doc/gitlab_basics/basicsimages/edit_file.png Binary files differnew file mode 100644 index 00000000000..afa68760108 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/edit_file.png diff --git a/doc/gitlab_basics/basicsimages/file_located.png b/doc/gitlab_basics/basicsimages/file_located.png Binary files differnew file mode 100644 index 00000000000..1def489d16b --- /dev/null +++ b/doc/gitlab_basics/basicsimages/file_located.png diff --git a/doc/gitlab_basics/basicsimages/file_name.png b/doc/gitlab_basics/basicsimages/file_name.png Binary files differnew file mode 100644 index 00000000000..9ac2f1c355f --- /dev/null +++ b/doc/gitlab_basics/basicsimages/file_name.png diff --git a/doc/gitlab_basics/basicsimages/find_file.png b/doc/gitlab_basics/basicsimages/find_file.png Binary files differnew file mode 100644 index 00000000000..98639149a39 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/find_file.png diff --git a/doc/gitlab_basics/basicsimages/find_group.png b/doc/gitlab_basics/basicsimages/find_group.png Binary files differnew file mode 100644 index 00000000000..5ac33c7e953 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/find_group.png diff --git a/doc/gitlab_basics/basicsimages/fork.png b/doc/gitlab_basics/basicsimages/fork.png Binary files differnew file mode 100644 index 00000000000..b1f94938613 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/fork.png diff --git a/doc/gitlab_basics/basicsimages/group_info.png b/doc/gitlab_basics/basicsimages/group_info.png Binary files differnew file mode 100644 index 00000000000..e78d84e4d80 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/group_info.png diff --git a/doc/gitlab_basics/basicsimages/groups.png b/doc/gitlab_basics/basicsimages/groups.png Binary files differnew file mode 100644 index 00000000000..b8104343afa --- /dev/null +++ b/doc/gitlab_basics/basicsimages/groups.png diff --git a/doc/gitlab_basics/basicsimages/https.png b/doc/gitlab_basics/basicsimages/https.png Binary files differnew file mode 100644 index 00000000000..2a31b4cf751 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/https.png diff --git a/doc/gitlab_basics/basicsimages/image_file.png b/doc/gitlab_basics/basicsimages/image_file.png Binary files differnew file mode 100644 index 00000000000..1061d9c5082 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/image_file.png diff --git a/doc/gitlab_basics/basicsimages/issue_title.png b/doc/gitlab_basics/basicsimages/issue_title.png Binary files differnew file mode 100644 index 00000000000..7b69c705392 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/issue_title.png diff --git a/doc/gitlab_basics/basicsimages/issues.png b/doc/gitlab_basics/basicsimages/issues.png Binary files differnew file mode 100644 index 00000000000..9354d05319e --- /dev/null +++ b/doc/gitlab_basics/basicsimages/issues.png diff --git a/doc/gitlab_basics/basicsimages/key.png b/doc/gitlab_basics/basicsimages/key.png Binary files differnew file mode 100644 index 00000000000..321805cda98 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/key.png diff --git a/doc/gitlab_basics/basicsimages/merge_requests.png b/doc/gitlab_basics/basicsimages/merge_requests.png Binary files differnew file mode 100644 index 00000000000..7601d40de47 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/merge_requests.png diff --git a/doc/gitlab_basics/basicsimages/new_issue.png b/doc/gitlab_basics/basicsimages/new_issue.png Binary files differnew file mode 100644 index 00000000000..94e7503dd8b --- /dev/null +++ b/doc/gitlab_basics/basicsimages/new_issue.png diff --git a/doc/gitlab_basics/basicsimages/new_merge_request.png b/doc/gitlab_basics/basicsimages/new_merge_request.png Binary files differnew file mode 100644 index 00000000000..9120d2b1ab1 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/new_merge_request.png diff --git a/doc/gitlab_basics/basicsimages/new_project.png b/doc/gitlab_basics/basicsimages/new_project.png Binary files differnew file mode 100644 index 00000000000..ac255270a66 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/new_project.png diff --git a/doc/gitlab_basics/basicsimages/newbranch.png b/doc/gitlab_basics/basicsimages/newbranch.png Binary files differnew file mode 100644 index 00000000000..da1a6b604ea --- /dev/null +++ b/doc/gitlab_basics/basicsimages/newbranch.png diff --git a/doc/gitlab_basics/basicsimages/paste_sshkey.png b/doc/gitlab_basics/basicsimages/paste_sshkey.png Binary files differnew file mode 100644 index 00000000000..9880ddfead1 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/paste_sshkey.png diff --git a/doc/gitlab_basics/basicsimages/profile_settings.png b/doc/gitlab_basics/basicsimages/profile_settings.png Binary files differnew file mode 100644 index 00000000000..5f2e7a7e10c --- /dev/null +++ b/doc/gitlab_basics/basicsimages/profile_settings.png diff --git a/doc/gitlab_basics/basicsimages/project_info.png b/doc/gitlab_basics/basicsimages/project_info.png Binary files differnew file mode 100644 index 00000000000..6c06ff351fa --- /dev/null +++ b/doc/gitlab_basics/basicsimages/project_info.png diff --git a/doc/gitlab_basics/basicsimages/public_file_link.png b/doc/gitlab_basics/basicsimages/public_file_link.png Binary files differnew file mode 100644 index 00000000000..1a60a3d880a --- /dev/null +++ b/doc/gitlab_basics/basicsimages/public_file_link.png diff --git a/doc/gitlab_basics/basicsimages/select_branch.png b/doc/gitlab_basics/basicsimages/select_branch.png Binary files differnew file mode 100644 index 00000000000..3475b2df576 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/select_branch.png diff --git a/doc/gitlab_basics/basicsimages/select_project.png b/doc/gitlab_basics/basicsimages/select_project.png Binary files differnew file mode 100644 index 00000000000..6d5aa439124 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/select_project.png diff --git a/doc/gitlab_basics/basicsimages/settings.png b/doc/gitlab_basics/basicsimages/settings.png Binary files differnew file mode 100644 index 00000000000..9bf9c5a0d39 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/settings.png diff --git a/doc/gitlab_basics/basicsimages/shh_keys.png b/doc/gitlab_basics/basicsimages/shh_keys.png Binary files differnew file mode 100644 index 00000000000..d7ef4dafe77 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/shh_keys.png diff --git a/doc/gitlab_basics/basicsimages/submit_new_issue.png b/doc/gitlab_basics/basicsimages/submit_new_issue.png Binary files differnew file mode 100644 index 00000000000..18944417085 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/submit_new_issue.png diff --git a/doc/gitlab_basics/basicsimages/title_description_mr.png b/doc/gitlab_basics/basicsimages/title_description_mr.png Binary files differnew file mode 100644 index 00000000000..e08eb628414 --- /dev/null +++ b/doc/gitlab_basics/basicsimages/title_description_mr.png diff --git a/doc/gitlab_basics/basicsimages/white_space.png b/doc/gitlab_basics/basicsimages/white_space.png Binary files differnew file mode 100644 index 00000000000..6363a09360e --- /dev/null +++ b/doc/gitlab_basics/basicsimages/white_space.png diff --git a/doc/gitlab_basics/start_using_git.md b/doc/gitlab_basics/start_using_git.md new file mode 100644 index 00000000000..f01a2f77eec --- /dev/null +++ b/doc/gitlab_basics/start_using_git.md @@ -0,0 +1,67 @@ +# Start using Git on the commandline + +If you want to start using a Git and GitLab, make sure that you have created an account on [gitlab.com](https://about.gitlab.com/) + +## Open a shell + +* Depending on your operating system, find the shell of your preference. Here are some suggestions + +- [Terminal](http://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line) on Mac OSX + +- [GitBash](https://msysgit.github.io) on Windows + +- [Linux Terminal](http://www.howtogeek.com/140679/beginner-geek-how-to-start-using-the-linux-terminal/) on Linux + +## Check if Git has already been installed + +* Git is usually preinstalled on Mac and Linux + +* Type the following command and then press enter + +``` +git --version +``` + +* You should receive a message that will tell you which Git version you have in your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) + +* If Git doesn't automatically download, there's an option on the website to [download manually](https://git-scm.com/downloads). Then follow the steps on the installation window + +* After you finished installing, open a new shell and type "git --version" again to verify that it was correctly installed + +## Add your Git username and set your email + +* It is important because every Git commit that you create will use this information + +* On your shell, type the following command to add your username + +``` +git config --global user.name ADD YOUR USERNAME +``` + +* Then verify that you have the correct username + +``` +git config --global user.name +``` + +* To set your email address, type the following command + +``` +git config --global user.email ADD YOUR EMAIL +``` + +* To verify that you entered your email correctly, type + +``` +git config --global user.email +``` + +* You'll need to do this only once because you are using the "--global" option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the "--global" option when you’re in that project + +## Check your information + +* To view the information that you entered, type + +``` +git config --global --list +``` diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index 00867ead80d..f7d4f3de68b 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -24,7 +24,7 @@ set up a custom hook. 1. Pick a project that needs a custom git hook. 1. On the GitLab server, navigate to the project's repository directory. -For a manual install the path is usually +For an installation from source the path is usually `/home/git/repositories/<group>/<project>.git`. For Omnibus installs the path is usually `/var/opt/gitlab/git-data/repositories/<group>/<project>.git`. 1. Create a new directory in this location called `custom_hooks`. diff --git a/doc/install/installation.md b/doc/install/installation.md index d107ea15946..8b918cba133 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -1,8 +1,14 @@ -# Installation +# Installation from source ## Consider the Omnibus package installation -Since a manual installation is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm). +Since an installation from source is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm). + +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. +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. ## Select Version to Install @@ -22,7 +28,9 @@ This is the official installation guide to set up a production server. To set up The following steps have been known to work. Please **use caution when you deviate** from this guide. Make sure you don't violate any assumptions GitLab makes about its environment. For example many people run into permission problems because they changed the location of directories or run services as the wrong user. -If you find a bug/error in this guide please **submit a merge request** following the [contributing guide](../../CONTRIBUTING.md). +If you find a bug/error in this guide please **submit a merge request** +following the +[contributing guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md). ## Overview @@ -54,7 +62,13 @@ up-to-date and install it. Install the required packages (needed to compile Ruby and native extensions to Ruby gems): - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake + sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs + +If you want to use Kerberos for user authentication, then install libkrb5-dev: + + sudo apt-get install libkrb5-dev + +**Note:** If you don't know what Kerberos is, you can assume you don't need it. Make sure you have the right version of Git installed @@ -74,8 +88,8 @@ Is the system packaged Git too old? Remove it and compile from source. # Download and compile from source cd /tmp - curl -L --progress https://www.kernel.org/pub/software/scm/git/git-2.1.2.tar.gz | tar xz - cd git-2.1.2/ + curl -L --progress https://www.kernel.org/pub/software/scm/git/git-2.4.3.tar.gz | tar xz + cd git-2.4.3/ ./configure make prefix=/usr/local all @@ -101,8 +115,8 @@ Remove the old Ruby 1.8 if present Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby - curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.5.tar.gz | tar xz - cd ruby-2.1.5 + curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz + cd ruby-2.1.6 ./configure --disable-install-rdoc make sudo make install @@ -139,7 +153,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Try connecting to the new database with the new user sudo -u git -H psql -d gitlabhq_production - + # Quit the database session gitlabhq_production> \q @@ -181,9 +195,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-5-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-12-stable gitlab -**Note:** You can change `7-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `7-12-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -227,10 +241,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Copy the example Rack attack config sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb - # Configure Git global settings for git user, useful when editing via web - # Edit user.email according to what is set in gitlab.yml - sudo -u git -H git config --global user.name "GitLab" - sudo -u git -H git config --global user.email "example@example.com" + # Configure Git global settings for git user, used when editing via web editor sudo -u git -H git config --global core.autocrlf input # Configure Redis connection settings @@ -268,17 +279,19 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da **Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](http://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2. # For PostgreSQL (note, the option says "without ... mysql") - sudo -u git -H bundle install --deployment --without development test mysql aws + sudo -u git -H bundle install --deployment --without development test mysql aws kerberos # Or if you use MySQL (note, the option says "without ... postgres") - sudo -u git -H bundle install --deployment --without development test postgres aws + sudo -u git -H bundle install --deployment --without development test postgres aws kerberos + +**Note:** If you want to use Kerberos for user authentication, then omit `kerberos` in the `--without` option above. ### Install GitLab Shell GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.2.0] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.3] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: @@ -286,6 +299,8 @@ GitLab Shell is an SSH access and repository management software developed speci **Note:** If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps. +**Note:** Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in /etc/hosts ("127.0.0.1 hostname"). This might be necessary for example if you set up gitlab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)". + ### Initialize Database and Activate Advanced Features sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production @@ -294,9 +309,9 @@ GitLab Shell is an SSH access and repository management software developed speci # When done you see 'Administrator account created:' -**Note:** You can set the Administrator password by supplying it in environmental variable `GITLAB_ROOT_PASSWORD`, eg.: +**Note:** You can set the Administrator/root password by supplying it in environmental variable `GITLAB_ROOT_PASSWORD` as seen below. If you don't set the password (and it is set to the default one) please wait with exposing GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login you'll be forced to change the default password. - sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=newpassword + sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword ### Install Init Script @@ -386,15 +401,17 @@ NOTE: Supply `SANITIZE=true` environment variable to `gitlab:check` to omit proj ### Initial Login -Visit YOUR_SERVER in your web browser for your first GitLab login. The setup has created an admin account for you. You can use it to log in: +Visit YOUR_SERVER in your web browser for your first GitLab login. The setup has created a default admin account for you. You can use it to log in: root 5iveL!fe -**Important Note:** Please go over to your profile page and immediately change the password, so nobody can access your GitLab by using this login information later on. +**Important Note:** On login you'll be prompted to change the password. **Enjoy!** +You can use `sudo service gitlab start` and `sudo service gitlab stop` to start and stop GitLab. + ## Advanced Setup Tips ### Using HTTPS @@ -458,4 +475,4 @@ You can configure LDAP authentication in `config/gitlab.yml`. Please restart Git ### Using Custom Omniauth Providers -See the [omniauth integration document](doc/integration/omniauth.md) +See the [omniauth integration document](../integration/omniauth.md) diff --git a/doc/install/requirements.md b/doc/install/requirements.md index fd59ac8a073..7a3216dd2d2 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -7,7 +7,7 @@ - Ubuntu - Debian - CentOS -- RedHat Enterprise Linux (please use the CentOS packages and instructions) +- Red Hat Enterprise Linux (please use the CentOS packages and instructions) - Scientific Linux (please use the CentOS packages and instructions) - Oracle Linux (please use the CentOS packages and instructions) @@ -22,7 +22,7 @@ For the installations options please see [the installation page on the GitLab we - FreeBSD On the above unsupported distributions is still possible to install GitLab yourself. -Please see the [manual installation guide](https://github.com/gitlabhq/gitlabhq/blob/master/doc/install/installation.md) and the [unofficial installation guides](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Unofficial-Installation-Guides) on the public wiki for more information. +Please see the [installation from source guide](https://github.com/gitlabhq/gitlabhq/blob/master/doc/install/installation.md) and the [unofficial installation guides](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Unofficial-Installation-Guides) on the public wiki for more information. ### Non-Unix operating systems such as Windows @@ -38,6 +38,16 @@ We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab ## Hardware requirements +### Storage + +The necessary hard drive space largely depends on the size of the repos you want to store in GitLab but as a *rule of thumb* you should have at least twice as much free space as all your repos combined take up. You need twice the storage because [GitLab satellites](structure.md) contain an extra copy of each repo. + +If you want to be flexible about growing your hard drive space in the future consider mounting it using LVM so you can add more hard drives when you need them. + +Apart from a local hard drive you can also mount a volume that supports the network file system (NFS) protocol. This volume might be located on a file server, a network attached storage (NAS) device, a storage area network (SAN) or on an Amazon Web Services (AWS) Elastic Block Store (EBS) volume. + +If you have enough RAM memory and a recent CPU the speed of GitLab is mainly limited by hard drive seek times. Having a fast drive (7200 RPM and up) or a solid state drive (SSD) will improve the responsiveness of GitLab. + ### CPU - 1 core works supports up to 100 users but the application can be a bit slower due to having all workers and background jobs running on the same core @@ -50,6 +60,10 @@ We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab ### Memory +You need at least 2GB of addressable memory (RAM + swap) to install and use GitLab! +With less memory GitLab will give strange errors during the reconfigure run and 500 errors during usage. + +- 512MB RAM + 1.5GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advise. - 1GB RAM + 1GB swap supports up to 100 users - **2GB RAM** is the **recommended** memory size and supports up to 500 users - 4GB RAM supports up to 2,000 users @@ -60,15 +74,19 @@ We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab 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. -### Storage +## Unicorn Workers -The necessary hard drive space largely depends on the size of the repos you want to store in GitLab but as a *rule of thumb* you should have at least twice as much free space as all your repos combined take up. You need twice the storage because [GitLab satellites](structure.md) contain an extra copy of each repo. +It's possible to increase the amount of unicorn workers and this will usually help for to reduce the response time of the applications and increase the ability to handle parallel requests. -If you want to be flexible about growing your hard drive space in the future consider mounting it using LVM so you can add more hard drives when you need them. +For most instances we recommend using: CPU cores + 1 = unicorn workers. +So for a machine with 2 cores, 3 unicorn workers is ideal. -Apart from a local hard drive you can also mount a volume that supports the network file system (NFS) protocol. This volume might be located on a file server, a network attached storage (NAS) device, a storage area network (SAN) or on an Amazon Web Services (AWS) Elastic Block Store (EBS) volume. +For all machines that have 1GB and up we recommend a minimum of three unicorn workers. +If you have a 512MB machine with a magnetic (non-SSD) swap drive we recommend to configure only one Unicorn worker to prevent excessive swapping. +With one Unicorn worker only git over ssh access will work because the git over HTTP access requires two running workers (one worker to receive the user request and one worker for the authorization check). +If you have a 512MB machine with a SSD drive you can use two Unicorn workers, this will allow HTTP access although it will be slow due to swapping. -If you have enough RAM memory and a recent CPU the speed of GitLab is mainly limited by hard drive seek times. Having a fast drive (7200 RPM and up) or a solid state drive (SSD) will improve the responsiveness of GitLab. +To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). ## Database @@ -88,4 +106,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o - Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/)) - Safari 7+ (known problem: required fields in html5 do not work) - Opera (Latest released version) -- IE 10+ +- IE 10+
\ No newline at end of file diff --git a/doc/integration/README.md b/doc/integration/README.md index 357ed038314..286bd34a0bd 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -6,14 +6,16 @@ See the documentation below for details on how to configure these services. - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP -- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, and Google via OAuth. +- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth. - [Slack](slack.md) Integrate with the Slack chat service +- [OAuth2 provider](oauth_provider.md) OAuth2 application creation +- [Gmail](gitlab_buttons_in_gmail.md) Adds GitLab actions to messages -Jenkins support is [available in GitLab EE](http://doc.gitlab.com/ee/integration/jenkins.html). +GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html). ## Project services -Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, PivotalTracker and Slack are available in the from of a Project Service. +Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a Project Service. You can find these within GitLab in the Services page under Project Settings if you are at least a master on the project. Project Services are a bit like plugins in that they allow a lot of freedom in adding functionality to GitLab, for example there is also a service that can send an email every time someone pushes new commits. Because GitLab is open source we can ship with the code and tests for all plugins. diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md new file mode 100644 index 00000000000..6a0fa4ce015 --- /dev/null +++ b/doc/integration/bitbucket.md @@ -0,0 +1,140 @@ +# Integrate your server with Bitbucket + +Import projects from Bitbucket and login to your GitLab instance with your Bitbucket account. + +To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket. +Bitbucket will generate an application ID and secret key for you to use. + +1. Sign in to Bitbucket. + +1. Navigate to your individual user settings or a team's settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or a team - that is entirely up to you. + +1. Select "OAuth" in the left menu. + +1. Select "Add consumer". + +1. Provide the required details. + - Name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or something else descriptive. + - Application description: Fill this in if you wish. + - URL: The URL to your GitLab installation. 'https://gitlab.company.com' +1. Select "Save". + +1. You should now see a Key and Secret in the list of OAuth customers. + Keep this page open as you continue configuration. + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: + + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "bitbucket", + "app_id" => "YOUR_KEY", + "app_secret" => "YOUR_APP_SECRET", + "url" => "https://bitbucket.org/" + } + ] + ``` + + For installation from source: + + ``` + - { name: 'bitbucket', app_id: 'YOUR_KEY', + app_secret: 'YOUR_APP_SECRET' } + ``` + +1. Change 'YOUR_APP_ID' to the key from the Bitbucket application page from step 7. + +1. Change 'YOUR_APP_SECRET' to the secret from the Bitbucket application page from step 7. + +1. Save the configuration file. + +1. If you're using the omnibus package, reconfigure GitLab (```gitlab-ctl reconfigure```). + +1. Restart GitLab for the changes to take effect. + +On the sign in page there should now be a Bitbucket icon below the regular sign in form. +Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application. +If everything goes well the user will be returned to GitLab and will be signed in. + +## Bitbucket project import + +To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com. + +Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key. + +### Step 1: Public key + +To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations. + +If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following: + +1. Create a new SSH key: + + ```sh + sudo -u git -H ssh-keygen + ``` + + When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`. + Make sure to use an **empty passphrase**. + +1. Configure SSH client to use your new key: + + Open the SSH configuration file of the git user. + + ```sh + sudo editor /home/git/.ssh/config + ``` + + Add a host configuration for `bitbucket.org`. + + ```sh + Host bitbucket.org + IdentityFile ~/.ssh/bitbucket_rsa + User git + ``` + +### Step 2: Known hosts + +To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so: + +1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use: + + ```sh + sudo -u git -H ssh bitbucket.org + ``` + +1. Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter): + + ```sh + The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established. + RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40. + Are you sure you want to continue connecting (yes/no)? + ``` + +1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts. + +1. Your GitLab server is now able to connect to Bitbucket over SSH. + +1. Restart GitLab to allow it to find the new public key. + +You should now see the "Import projects from Bitbucket" option on the New Project page enabled. diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 87af94512ed..3e660cfba1e 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -1,13 +1,44 @@ # External issue tracker -GitLab has a great issue tracker but you can also use an external issue tracker such as JIRA, Bugzilla or Redmine. This is something that you can turn on per GitLab project. If for example you configure JIRA it provides the following functionality: +GitLab has a great issue tracker but you can also use an external issue tracker such as Jira, Bugzilla or Redmine. You can configure issue trackers per GitLab project. For instance, if you configure Jira it allows you to do the following: -- the 'Issues' link on the GitLab project pages takes you to the appropriate JIRA issue index; -- clicking 'New issue' on the project dashboard creates a new JIRA issue; -- To reference JIRA issue PROJECT-1234 in comments, use syntax PROJECT-1234. Commit messages get turned into HTML links to the corresponding JIRA issue. +- the 'Issues' link on the GitLab project pages takes you to the appropriate Jira issue index; +- clicking 'New issue' on the project dashboard creates a new Jira issue; +- To reference Jira issue PROJECT-1234 in comments, use syntax PROJECT-1234. Commit messages get turned into HTML links to the corresponding Jira issue. - + -You can configure the integration in the gitlab.yml configuration file. +GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html). + +## Configuration + +### Project Service + +You can enable an external issue tracker per project. As an example, we will configure `Redmine` for project named gitlab-ci. + +Fill in the required details on the page: + + + +* `description` A name for the issue tracker (to differentiate between instances, for example). +* `project_url` The URL to the project in Redmine which is being linked to this GitLab project. +* `issues_url` The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the url. This id is used by GitLab as a placeholder to replace the issue number. +* `new_issue_url` This is the URL to create a new issue in Redmine for the project linked to this GitLab project. + +### Service Template + +It is necessary to configure the external issue tracker per project, because project specific details are needed for the integration with GitLab. +The admin can add a service template that sets a default for each project. This makes it much easier to configure individual projects. + +In GitLab Admin section, navigate to `Service Templates` and choose the service template you want to create: + + + +After the template is created, the template details will be pre-filled on the project service page. + +NOTE: For each project, you will still need to configure the issue tracking URLs by replacing `:issues_tracker_id` in the above screenshot +with the ID used by your external issue tracker. Prior to GitLab v7.8, this ID was configured in the project settings, and GitLab would automatically +update the URL configured in `gitlab.yml`. This behavior is now depecated, and all issue tracker URLs must be configured directly +within the project's Services settings. Support to add your commits to the Jira ticket automatically is [available in GitLab EE](http://doc.gitlab.com/ee/integration/jira.html). diff --git a/doc/integration/github.md b/doc/integration/github.md index 714593d8266..b64501c2aaa 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -1,6 +1,9 @@ -# GitHub OAuth2 OmniAuth Provider +# Integrate your server with GitHub -To enable the GitHub OmniAuth provider you must register your application with GitHub. GitHub will generate a client ID and secret key for you to use. +Import projects from GitHub and login to your GitLab instance with your GitHub account. + +To enable the GitHub OmniAuth provider you must register your application with GitHub. +GitHub will generate an application ID and secret key for you to use. 1. Sign in to GitHub. @@ -14,35 +17,63 @@ To enable the GitHub OmniAuth provider you must register your application with G - Application name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or something else descriptive. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - - Authorization callback URL: 'https://gitlab.company.com/users/auth/github/callback' + - Authorization callback URL: 'https://gitlab.company.com/' 1. Select "Register application". -1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). Keep this page open as you continue configuration.  +1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). + Keep this page open as you continue configuration. +  1. On your GitLab server, open the configuration file. + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: + ```sh - cd /home/git/gitlab + cd /home/git/gitlab - sudo -u git -H editor config/gitlab.yml + sudo -u git -H editor config/gitlab.yml ``` -1. Find the section dealing with OmniAuth. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for more details. +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "github", + "app_id" => "YOUR_APP_ID", + "app_secret" => "YOUR_APP_SECRET", + "url" => "https://github.com/", + "args" => { "scope" => "user:email" } + } + ] + ``` -1. Under `providers:` uncomment (or add) lines that look like the following: + For installation from source: ``` - - { name: 'github', app_id: 'YOUR APP ID', - app_secret: 'YOUR APP SECRET', - args: { scope: 'user:email' } } + - { name: 'github', app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET', + args: { scope: 'user:email' } } ``` -1. Change 'YOUR APP ID' to the client ID from the GitHub application page from step 7. +1. Change 'YOUR_APP_ID' to the client ID from the GitHub application page from step 7. -1. Change 'YOUR APP SECRET' to the client secret from the GitHub application page from step 7. +1. Change 'YOUR_APP_SECRET' to the client secret from the GitHub application page from step 7. 1. Save the configuration file. 1. Restart GitLab for the changes to take effect. -On the sign in page there should now be a GitHub icon below the regular sign in form. Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. +On the sign in page there should now be a GitHub icon below the regular sign in form. +Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application. +If everything goes well the user will be returned to GitLab and will be signed in. diff --git a/doc/integration/github_app.png b/doc/integration/github_app.png Binary files differindex c0873b2e20d..d890345ced9 100644 --- a/doc/integration/github_app.png +++ b/doc/integration/github_app.png diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md new file mode 100644 index 00000000000..216f1f11a9b --- /dev/null +++ b/doc/integration/gitlab.md @@ -0,0 +1,84 @@ +# Integrate your server with GitLab.com + +Import projects from GitLab.com and login to your GitLab instance with your GitLab.com account. + +To enable the GitLab.com OmniAuth provider you must register your application with GitLab.com. +GitLab.com will generate an application ID and secret key for you to use. + +1. Sign in to GitLab.com + +1. Navigate to your profile settings. + +1. Select "Applications" in the left menu. + +1. Select "New application". + +1. Provide the required details. + - Name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or something else descriptive. + - Redirect URI: + + ``` + http://your-gitlab.example.com/import/gitlab/callback + http://your-gitlab.example.com/users/auth/gitlab/callback + ``` + + The first link is required for the importer and second for the authorization. + +1. Select "Submit". + +1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). + Keep this page open as you continue configuration. +  + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: + + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "gitlab", + "app_id" => "YOUR_APP_ID", + "app_secret" => "YOUR_APP_SECRET", + "args" => { "scope" => "api" } + } + ] + ``` + + For installations from source: + + ``` + - { name: 'gitlab', app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET', + args: { scope: 'api' } } + ``` + +1. Change 'YOUR_APP_ID' to the Application ID from the GitLab.com application page. + +1. Change 'YOUR_APP_SECRET' to the secret from the GitLab.com application page. + +1. Save the configuration file. + +1. Restart GitLab for the changes to take effect. + +On the sign in page there should now be a GitLab.com icon below the regular sign in form. +Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application. +If everything goes well the user will be returned to your GitLab instance and will be signed in. diff --git a/doc/integration/gitlab_app.png b/doc/integration/gitlab_app.png Binary files differnew file mode 100644 index 00000000000..3f9391a821b --- /dev/null +++ b/doc/integration/gitlab_app.png diff --git a/doc/integration/gitlab_buttons_in_gmail.md b/doc/integration/gitlab_buttons_in_gmail.md index 5cfea5a90f8..e35bb8ba693 100644 --- a/doc/integration/gitlab_buttons_in_gmail.md +++ b/doc/integration/gitlab_buttons_in_gmail.md @@ -1,4 +1,4 @@ -# GitLab buttons in gmail +# GitLab buttons in Gmail GitLab supports [Google actions in email](https://developers.google.com/gmail/markup/actions/actions-overview). @@ -7,5 +7,22 @@ If correctly setup, emails that require an action will be marked in Gmail.  To get this functioning, you need to be registered with Google. -[See how to register with google in this document.](https://developers.google.com/gmail/markup/registering-with-google) +[See how to register with Google in this document.](https://developers.google.com/gmail/markup/registering-with-google) +To aid the registering with Google, GitLab offers a rake task that will send an email to Google whitelisting email address from your GitLab server. + +To check what would be sent to the Google email address, run the rake task: + +```bash +bundle exec rake gitlab:mail_google_schema_whitelisting RAILS_ENV=production +``` + +**This will not send the email but give you the output of how the mail will look.** + +Copy the output of the rake task to [Google email markup tester](https://www.google.com/webmasters/markup-tester/u/0/) and press "Validate". + +If you receive "No errors detected" message from the tester you can send the email using: + +```bash +bundle exec rake gitlab:mail_google_schema_whitelisting RAILS_ENV=production SEND=true +``` diff --git a/doc/integration/google.md b/doc/integration/google.md index 7a78aff8ea4..e1c14c7c948 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -27,27 +27,50 @@ To enable the Google OAuth2 OmniAuth provider you must register your application - Authorized redirect URI: 'https://gitlab.example.com/users/auth/google_oauth2/callback' 1. Under the heading "Client ID for web application" you should see a Client ID and Client secret (see screenshot). Keep this page open as you continue configuration.  -1. On your GitLab server, open the configuration file. +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: ```sh - cd /home/git/gitlab + cd /home/git/gitlab - sudo -u git -H editor config/gitlab.yml + sudo -u git -H editor config/gitlab.yml ``` -1. Find the section dealing with OmniAuth. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for more details. +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "google_oauth2", + "app_id" => "YOUR_APP_ID", + "app_secret" => "YOUR_APP_SECRET", + "args" => { "access_type" => "offline", "approval_prompt" => '' } + } + ] + ``` -1. Under `providers:` uncomment (or add) lines that look like the following: + For installations from source: ``` - - { name: 'google_oauth2', app_id: 'YOUR APP ID', - app_secret: 'YOUR APP SECRET', - args: { access_type: 'offline', approval_prompt: '' } } + - { name: 'google_oauth2', app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET', + args: { access_type: 'offline', approval_prompt: '' } } ``` -1. Change 'YOUR APP ID' to the client ID from the GitHub application page from step 7. +1. Change 'YOUR_APP_ID' to the client ID from the Google Developer page from step 10. -1. Change 'YOUR APP SECRET' to the client secret from the GitHub application page from step 7. +1. Change 'YOUR_APP_SECRET' to the client secret from the Google Developer page from step 10. 1. Save the configuration file. diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 56b0d826adb..904d5d7fee2 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -6,6 +6,13 @@ The first time a user signs in with LDAP credentials, GitLab will create a new G GitLab user attributes such as nickname and email will be copied from the LDAP user entry. +## Security + +GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email' or 'userPrincipalName' attribute. +An LDAP user who is allowed to change their email on the LDAP server can [take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users) on your GitLab server. + +We recommend against using GitLab LDAP integration if your LDAP users are allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on the LDAP server. + ## Configuring GitLab for LDAP integration To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. @@ -29,9 +36,9 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server label: 'LDAP' host: '_your_ldap_server' - port: 636 + port: 389 uid: 'sAMAccountName' - method: 'ssl' # "tls" or "ssl" or "plain" + method: 'plain' # "tls" or "ssl" or "plain" bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' password: '_the_password_of_the_bind_user' @@ -51,6 +58,11 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server # disable this setting, because the userPrincipalName contains an '@'. allow_username_or_email_login: false + # To maintain tight control over the number of active users on your GitLab installation, + # enable this setting to keep new users blocked until they have been cleared by the admin + # (default: false). + block_auto_created_users: false + # Base where we can search for users # # Ex. ou=People,dc=gitlab,dc=example @@ -76,6 +88,9 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server EOS ``` +If you are getting 'Connection Refused' errors when trying to connect to the LDAP server please double-check the LDAP `port` and `method` settings used by GitLab. +Common combinations are `method: 'plain'` and `port: 389`, OR `method: 'ssl'` and `port: 636`. + If you are using a GitLab installation from source you can find the LDAP settings in `/home/git/gitlab/config/gitlab.yml`: ``` diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md new file mode 100644 index 00000000000..192c321f712 --- /dev/null +++ b/doc/integration/oauth_provider.md @@ -0,0 +1,35 @@ +## GitLab as OAuth2 authentication service provider + +This document is about using GitLab as an OAuth authentication service provider to sign into other services. +If you want to use other OAuth authentication service providers to sign into GitLab please see the [OAuth2 client documentation](../api/oauth2.md) + +OAuth2 provides client applications a 'secure delegated access' to server resources on behalf of a resource owner. Or you can allow users to sign in to your application with their GitLab.com account. +In fact OAuth allows to issue access token to third-party clients by an authorization server, +with the approval of the resource owner, or end-user. +Mostly, OAuth2 is using for SSO (Single sign-on). But you can find a lot of different usages for this functionality. +For example, our feature 'GitLab Importer' is using OAuth protocol to give an access to repositories without sharing user credentials to GitLab.com account. +Also GitLab.com application can be used for authentication to your GitLab instance if needed [GitLab OmniAuth](gitlab.md). + +GitLab has two ways to add new OAuth2 application to an instance, you can add application as regular user and through admin area. So GitLab actually can have an instance-wide and a user-wide applications. There is no defferences between them except the different permission levels. + +### Adding application through profile +Go to your profile section 'Application' and press button 'New Application' + + + +After this you will see application form, where "Name" is arbitrary name, "Redirect URI" is URL in your app where users will be sent after authorization on GitLab.com. + + + +### Authorized application +Every application you authorized will be shown in your "Authorized application" sections. + + + +At any time you can revoke access just clicking button "Revoke" + +### OAuth applications in admin area + +If you want to create application that does not belong to certain user you can create it from admin area + +
\ No newline at end of file diff --git a/doc/integration/oauth_provider/admin_application.png b/doc/integration/oauth_provider/admin_application.png Binary files differnew file mode 100644 index 00000000000..a5f34512aa8 --- /dev/null +++ b/doc/integration/oauth_provider/admin_application.png diff --git a/doc/integration/oauth_provider/application_form.png b/doc/integration/oauth_provider/application_form.png Binary files differnew file mode 100644 index 00000000000..ae135db2627 --- /dev/null +++ b/doc/integration/oauth_provider/application_form.png diff --git a/doc/integration/oauth_provider/authorized_application.png b/doc/integration/oauth_provider/authorized_application.png Binary files differnew file mode 100644 index 00000000000..d3ce05be9cc --- /dev/null +++ b/doc/integration/oauth_provider/authorized_application.png diff --git a/doc/integration/oauth_provider/user_wide_applications.png b/doc/integration/oauth_provider/user_wide_applications.png Binary files differnew file mode 100644 index 00000000000..719e1974068 --- /dev/null +++ b/doc/integration/oauth_provider/user_wide_applications.png diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 00adae58dfa..8e2a602ec35 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -1,18 +1,47 @@ # OmniAuth -GitLab leverages OmniAuth to allow users to sign in using Twitter, GitHub, and other popular services. Configuring +GitLab leverages OmniAuth to allow users to sign in using Twitter, GitHub, and other popular services. -OmniAuth does not prevent standard GitLab authentication or LDAP (if configured) from continuing to work. Users can choose to sign in using any of the configured mechanisms. +Configuring OmniAuth does not prevent standard GitLab authentication or LDAP (if configured) from continuing to work. Users can choose to sign in using any of the configured mechanisms. - [Initial OmniAuth Configuration](#initial-omniauth-configuration) - [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) ## Initial OmniAuth Configuration -Before configuring individual OmniAuth providers there are a few global settings that need to be verified. +Before configuring individual OmniAuth providers there are a few global settings that are in common for all providers that we need to consider. -1. Open the configuration file. +- Omniauth needs to be enabled, see details below for example. +- `allow_single_sign_on` defaults to `false`. If `false` users must be created manually or they will not be able to +sign in via OmniAuth. +- `block_auto_created_users` defaults to `true`. If `true` auto created users will be blocked by default and will +have to be unblocked by an administrator before they are able to sign in. +- **Note:** If you set `allow_single_sign_on` to `true` and `block_auto_created_users` to `false` please be aware +that any user on the Internet will be able to successfully sign in to your GitLab without administrative approval. + +If you want to change these settings: + +* **For omnibus package** + + Open the configuration file: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + and change + + ``` + gitlab_rails['omniauth_enabled'] = true + gitlab_rails['omniauth_allow_single_sign_on'] = false + gitlab_rails['block_auto_created_users'] = true + ``` + +* **For installations from source** + + Open the configuration file: ```sh cd /home/git/gitlab @@ -20,13 +49,13 @@ Before configuring individual OmniAuth providers there are a few global settings sudo -u git -H editor config/gitlab.yml ``` -1. Find the section dealing with OmniAuth. The section will look similar to the following. + and change the following section ``` - ## OmniAuth settings + ## OmniAuth settings omniauth: # Allow login via Twitter, Google, etc. using OmniAuth providers - enabled: false + enabled: true # CAUTION! # This allows users to login without having a user account first (default: false). @@ -34,50 +63,19 @@ Before configuring individual OmniAuth providers there are a few global settings allow_single_sign_on: false # Locks down those users until they have been cleared by the admin (default: true). block_auto_created_users: true - - ## Auth providers - # Uncomment the following lines and fill in the data of the auth provider you want to use - # If your favorite auth provider is not listed you can use others: - # see https://github.com/gitlabhq/gitlab-public-wiki/wiki/Custom-omniauth-provider-configurations - # The 'app_id' and 'app_secret' parameters are always passed as the first two - # arguments, followed by optional 'args' which can be either a hash or an array. - providers: - # - { name: 'google_oauth2', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET', - # args: { access_type: 'offline', approval_prompt: '' } } - # - { name: 'twitter', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET'} - # - { name: 'github', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET', - # args: { scope: 'user:email' } } - # - {"name": 'shibboleth', - # args: { shib_session_id_field: "HTTP_SHIB_SESSION_ID", - # shib_application_id_field: "HTTP_SHIB_APPLICATION_ID", - # uid_field: "HTTP_EPPN", - # name_field: "HTTP_CN", - # info_fields: {"email": "HTTP_MAIL" } } } - ``` -1. Change `enabled` to `true`. - -1. Consider the next two configuration options: `allow_single_sign_on` and `block_auto_created_users`. - - - `allow_single_sign_on` defaults to `false`. If `false` users must be created manually or they will not be able to - sign in via OmniAuth. - - `block_auto_created_users` defaults to `true`. If `true` auto created users will be blocked by default and will - have to be unblocked by an administrator before they are able to sign in. - - **Note:** If you set `allow_single_sign_on` to `true` and `block_auto_created_users` to `false` please be aware - that any user on the Internet will be able to successfully sign in to your GitLab without administrative approval. - -1. Choose one or more of the Supported Providers below to continue configuration. +Now we can choose one or more of the Supported Providers below to continue configuration. ## Supported Providers - [GitHub](github.md) +- [Bitbucket](bitbucket.md) +- [GitLab.com](gitlab.md) - [Google](google.md) - [Shibboleth](shibboleth.md) - [Twitter](twitter.md) +- [SAML](saml.md) ## Enable OmniAuth for an Existing User diff --git a/doc/integration/redmine_configuration.png b/doc/integration/redmine_configuration.png Binary files differnew file mode 100644 index 00000000000..6b145363229 --- /dev/null +++ b/doc/integration/redmine_configuration.png diff --git a/doc/integration/redmine_service_template.png b/doc/integration/redmine_service_template.png Binary files differnew file mode 100644 index 00000000000..1159eb5b964 --- /dev/null +++ b/doc/integration/redmine_service_template.png diff --git a/doc/integration/saml.md b/doc/integration/saml.md new file mode 100644 index 00000000000..a8cc5c8f74a --- /dev/null +++ b/doc/integration/saml.md @@ -0,0 +1,77 @@ +# SAML OmniAuth Provider + +GitLab can be configured to act as a SAML 2.0 Service Provider (SP). This allows GitLab to consume assertions from a SAML 2.0 Identity Provider (IdP) such as Microsoft ADFS to authenticate users. + +First configure SAML 2.0 support in GitLab, then register the GitLab application in your SAML IdP: + +1. Make sure GitLab is configured with HTTPS. See [Using HTTPS](../install/installation.md#using-https) for instructions. + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: + + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "saml", + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + } + } + ] + ``` + + For installations from source: + + ```yaml + - { name: 'saml', + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + } } + ``` + +1. Change the value for 'assertion_consumer_service_url' to match the HTTPS endpoint of GitLab (append 'users/auth/saml/callback' to the HTTPS URL of your GitLab installation to generate the correct value). + +1. Change the values of 'idp_cert_fingerprint', 'idp_sso_target_url', 'name_identifier_format' to match your IdP. Check [the omniauth-saml documentation](https://github.com/PracticallyGreen/omniauth-saml) for details on these options. + +1. Change the value of 'issuer' to a unique name, which will identify the application to the IdP. + +1. Restart GitLab for the changes to take effect. + +1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified in 'issuer'. + +To ease configuration, most IdP accept a metadata URL for the application to provide configuration information to the IdP. To build the metadata URL for GitLab, append 'users/auth/saml/metadata' to the HTTPS URL of your GitLab installation, for instance: + ``` + https://gitlab.example.com/users/auth/saml/metadata + ``` + +At a minimum the IdP *must* provide a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username. GitLab will also use claims with name 'name', 'first_name', 'last_name' (see [the omniauth-saml gem](https://github.com/PracticallyGreen/omniauth-saml/blob/master/lib/omniauth/strategies/saml.rb) for supported claims). + +On the sign in page there should now be a SAML button below the regular sign in form. Click the icon to begin the authentication process. If everything goes well the user will be returned to GitLab and will be signed in. + diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md index 78317a5c0f2..6258e5f1030 100644 --- a/doc/integration/shibboleth.md +++ b/doc/integration/shibboleth.md @@ -2,19 +2,19 @@ This documentation is for enabling shibboleth with gitlab-omnibus package. -In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however I did not found way to easily configure nginx that is bundled in gitlab-omnibus package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider. +In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however I did not found way to easily configure Nginx that is bundled in gitlab-omnibus package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider. To enable the Shibboleth OmniAuth provider you must: -1. Configure Apache shibboleth module. Installation and configuration of module it self is out of scope of this document. +1. Configure Apache shibboleth module. Installation and configuration of module it self is out of scope of this document. Check https://wiki.shibboleth.net/ for more info. -1. You can find Apache config in gitlab-reciepes (https://github.com/gitlabhq/gitlab-recipes/blob/master/web-server/apache/gitlab-ssl.conf) +1. You can find Apache config in gitlab-recipes (https://github.com/gitlabhq/gitlab-recipes/blob/master/web-server/apache/gitlab-ssl.conf) Following changes are needed to enable shibboleth: -protect omniauth-shibboleth callback url: +protect omniauth-shibboleth callback URL: ``` <Location /users/auth/shibboleth/callback> AuthType shibboleth @@ -32,25 +32,25 @@ protect omniauth-shibboleth callback url: SetHandler shib </Location> ``` -exclude shibboleth urls from rewriting, add "RewriteCond %{REQUEST_URI} !/Shibboleth.sso" and "RewriteCond %{REQUEST_URI} !/shibboleth-sp", config should look like this: +exclude shibboleth URLs from rewriting, add "RewriteCond %{REQUEST_URI} !/Shibboleth.sso" and "RewriteCond %{REQUEST_URI} !/shibboleth-sp", config should look like this: ``` - #apache equivalent of nginx try files + # Apache equivalent of Nginx try files RewriteEngine on RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_URI} !/Shibboleth.sso - RewriteCond %{REQUEST_URI} !/shibboleth-sp + RewriteCond %{REQUEST_URI} !/Shibboleth.sso + RewriteCond %{REQUEST_URI} !/shibboleth-sp RewriteRule .* http://127.0.0.1:8080%{REQUEST_URI} [P,QSA] RequestHeader set X_FORWARDED_PROTO 'https' ``` -1. Edit /etc/gitlab/gitlab.rb configuration file, your shibboleth attributes should be in form of "HTTP_ATTRIBUTE" and you should addjust them to your need and environment. Add any other configuration you need. +1. Edit /etc/gitlab/gitlab.rb configuration file, your shibboleth attributes should be in form of "HTTP_ATTRIBUTE" and you should addjust them to your need and environment. Add any other configuration you need. -File it should look like this: +File should look like this: ``` external_url 'https://gitlab.example.com' gitlab_rails['internal_api_url'] = 'https://gitlab.example.com' -# disable nginx +# disable Nginx nginx['enable'] = false gitlab_rails['omniauth_allow_single_sign_on'] = true @@ -70,7 +70,7 @@ gitlab_rails['omniauth_providers'] = [ ] ``` -1. Save changes and reconfigure gitlab: +1. Save changes and reconfigure gitlab: ``` sudo gitlab-ctl reconfigure ``` diff --git a/doc/integration/slack.md b/doc/integration/slack.md index f2e73f272ef..84f1d74c058 100644 --- a/doc/integration/slack.md +++ b/doc/integration/slack.md @@ -16,7 +16,7 @@ To enable Slack integration you must create an Incoming WebHooks integration on 1. Choose the channel name you want to send notifications to -1. Click **Add Incoming WebHooks Integration**Add Integrations. +1. Click **Add Incoming WebHooks Integration** - Optional step; You can change bot's name and avatar by clicking modifying the bot name or avatar under **Integration Settings**. 1. Copy the **Webhook URL**, we'll need this later for GitLab. @@ -32,10 +32,15 @@ After Slack is ready we need to setup GitLab. Here are the steps to achieve this 1. Navigate to Settings -> Services -> Slack -1. Fill in your Slack details +1. Pick the triggers you want to activate +1. Fill in your Slack details + - Webhook: Paste the Webhook URL from the step above + - Username: Fill this in if you want to change the username of the bot + - Channel: Fill this in if you want to change the channel where the messages will be posted - Mark it as active - - Paste in the webhook url you got from Slack + +1. Save your settings Have fun :) diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index d1b52927d30..fe9091ad9a8 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -13,7 +13,7 @@ To enable the Twitter OmniAuth provider you must register your application with something else descriptive. - Description: Create a description. - Website: The URL to your GitLab installation. 'https://gitlab.example.com' - - Callback URL: 'https://gitlab.example.com/users/auth/github/callback' + - Callback URL: 'https://gitlab.example.com/users/auth/twitter/callback' - Agree to the "Rules of the Road."  @@ -33,25 +33,46 @@ To enable the Twitter OmniAuth provider you must register your application with 1. On your GitLab server, open the configuration file. + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: + ```sh - cd /home/git/gitlab + cd /home/git/gitlab - sudo -u git -H editor config/gitlab.yml + sudo -u git -H editor config/gitlab.yml ``` -1. Find the section dealing with OmniAuth. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) -for more details. +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "twitter", + "app_id" => "YOUR_APP_ID", + "app_secret" => "YOUR_APP_SECRET" + } + ] + ``` -1. Under `providers:` uncomment (or add) lines that look like the following: + For installations from source: ``` - - { name: 'twitter', app_id: 'YOUR APP ID', - app_secret: 'YOUR APP SECRET' } + - { name: 'twitter', app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET' } ``` -1. Change 'YOUR APP ID' to the API key from Twitter page in step 11. +1. Change 'YOUR_APP_ID' to the API key from Twitter page in step 11. -1. Change 'YOUR APP SECRET' to the API secret from the Twitter page in step 11. +1. Change 'YOUR_APP_SECRET' to the API secret from the Twitter page in step 11. 1. Save the configuration file. diff --git a/doc/logs/logs.md b/doc/logs/logs.md new file mode 100644 index 00000000000..83c32b09253 --- /dev/null +++ b/doc/logs/logs.md @@ -0,0 +1,102 @@ +## 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) + +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. + +#### production.log +This file lives in `/var/log/gitlab/gitlab-rails/production.log` for omnibus package or in `/home/git/gitlab/log/production.log` for installations from the source. + +This file contains information about all performed requests. You can see url and type of request, IP address and what exactly parts of code were involved to service this particular request. Also you can see all SQL request that have been performed and how much time it took. +This task is more useful for GitLab contributors and developers. Use part of this log file when you are going to report bug. + +``` +Started GET "/gitlabhq/yaml_db/tree/master" for 168.111.56.1 at 2015-02-12 19:34:53 +0200 +Processing by Projects::TreeController#show as HTML + Parameters: {"project_id"=>"gitlabhq/yaml_db", "id"=>"master"} + + ... [CUT OUT] + + amespaces"."created_at" DESC, "namespaces"."id" DESC LIMIT 1[0m [["id", 26]] + [1m[35mCACHE (0.0ms)[0m SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $1 AND "members"."source_type" = $2 AND "members"."user_id" = 1 ORDER BY "members"."created_at" DESC, "members"."id" DESC LIMIT 1 [["source_id", 18], ["source_type", "Project"]] + [1m[36mCACHE (0.0ms)[0m [1mSELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members". + [1m[36m (1.4ms)[0m [1mSELECT COUNT(*) FROM "merge_requests" WHERE "merge_requests"."target_project_id" = $1 AND ("merge_requests"."state" IN ('opened','reopened'))[0m [["target_project_id", 18]] + Rendered layouts/nav/_project.html.haml (28.0ms) + Rendered layouts/_collapse_button.html.haml (0.2ms) + Rendered layouts/_flash.html.haml (0.1ms) + Rendered layouts/_page.html.haml (32.9ms) +Completed 200 OK in 166ms (Views: 117.4ms | ActiveRecord: 27.2ms) +``` +In this example we can see that server processed HTTP request with url `/gitlabhq/yaml_db/tree/master` from IP 168.111.56.1 at 2015-02-12 19:34:53 +0200. Also we can see that request was processed by Projects::TreeController. + +#### application.log +This file lives in `/var/log/gitlab/gitlab-rails/application.log` for omnibus package or in `/home/git/gitlab/log/application.log` for installations from the source. + +This log file helps you discover events happening in your instance such as user creation, project removing and so on. + +``` +October 06, 2014 11:56: User "Administrator" (admin@example.com) was created +October 06, 2014 11:56: Documentcloud created a new project "Documentcloud / Underscore" +October 06, 2014 11:56: Gitlab Org created a new project "Gitlab Org / Gitlab Ce" +October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was removed +October 07, 2014 11:25: Project "project133" was removed +``` +#### githost.log +This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for omnibus package or in `/home/git/gitlab/log/githost.log` for installations from the source. + +The GitLab has to interact with git repositories but in some rare cases something can go wrong and in this case you will know what exactly happened. This log file contains all failed requests from GitLab to git repository. In majority of cases this file will be useful for developers only. +``` +December 03, 2014 13:20 -> ERROR -> Command failed [1]: /usr/bin/git --git-dir=/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/group184/gitlabhq/.git --work-tree=/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/group184/gitlabhq merge --no-ff -mMerge branch 'feature_conflict' into 'feature' source/feature_conflict + +error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/repositories/gitlabhq/gitlab_git.git' +``` + +#### satellites.log +This file lives in `/var/log/gitlab/gitlab-rails/satellites.log` for omnibus package or in `/home/git/gitlab/log/satellites.log` for installations from the source. + +In some cases GitLab should perform write actions to git repository, for example when it is needed to merge the merge request or edit a file with online editor. If something went wrong you can look into this file to find out what exactly happened. +``` +October 07, 2014 11:36: Failed to create satellite for Chesley Weimann III / project1817 +October 07, 2014 11:36: PID: 1872: git clone /Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/repositories/conrad6841/gitlabhq.git /Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/conrad6841/gitlabhq +October 07, 2014 11:36: PID: 1872: -> fatal: repository '/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/repositories/conrad6841/gitlabhq.git' does not exist +``` + +#### sidekiq.log +This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for omnibus package or in `/home/git/gitlab/log/sidekiq.log` for installations from the source. + +GitLab uses background jobs for processing tasks which can take a long time. All information about processing these jobs are writing down to this file. +``` +2014-06-10T07:55:20Z 2037 TID-tm504 ERROR: /opt/bitnami/apps/discourse/htdocs/vendor/bundle/ruby/1.9.1/gems/redis-3.0.7/lib/redis/client.rb:228:in `read' +2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"} +``` + +#### gitlab-shell.log +This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for installations from the source. + +gitlab-shell is using by Gitlab for executing git commands and provide ssh access to git repositories. + +``` +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. +``` + +#### 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. + +``` +I, [2015-02-13T06:14:46.680381 #9047] INFO -- : Refreshing Gem list +I, [2015-02-13T06:14:56.931002 #9047] INFO -- : listening on addr=127.0.0.1:8080 fd=12 +I, [2015-02-13T06:14:56.931381 #9047] INFO -- : listening on addr=/var/opt/gitlab/gitlab-rails/sockets/gitlab.socket fd=13 +I, [2015-02-13T06:14:56.936638 #9047] INFO -- : master process ready +I, [2015-02-13T06:14:56.946504 #9092] INFO -- : worker=0 spawned pid=9092 +I, [2015-02-13T06:14:56.946943 #9092] INFO -- : worker=0 ready +I, [2015-02-13T06:14:56.947892 #9094] INFO -- : worker=1 spawned pid=9094 +I, [2015-02-13T06:14:56.948181 #9094] INFO -- : worker=1 ready +W, [2015-02-13T07:16:01.312916 #9094] WARN -- : #<Unicorn::HttpServer:0x0000000208f618>: worker (pid: 9094) exceeds memory limit (320626688 bytes > 247066940 bytes) +W, [2015-02-13T07:16:01.313000 #9094] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 9094) alive: 3621 sec (trial 1) +I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped #<Process::Status: pid 9094 exit 0> worker=1 +I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379 +I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready +```
\ No newline at end of file diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index edb7a975503..9c7f723c06d 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -6,7 +6,7 @@ * [Newlines](#newlines) * [Multiple underscores in words](#multiple-underscores-in-words) -* [URL autolinking](#url-autolinking) +* [URL auto-linking](#url-auto-linking) * [Code and Syntax Highlighting](#code-and-syntax-highlighting) * [Emoji](#emoji) * [Special GitLab references](#special-gitlab-references) @@ -40,20 +40,21 @@ You can use GFM in - milestones - wiki pages -You can also use other rich text files in GitLab. You might have to install a depency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information. +You can also use other rich text files in GitLab. You might have to install a dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information. ## Newlines GFM honors the markdown specification in how [paragraphs and line breaks are handled](http://daringfireball.net/projects/markdown/syntax#p). -A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.: +A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. +Line-breaks, or softreturns, are rendered if you end a line with two or more spaces - Roses are red + Roses are red [followed by two or more spaces] Violets are blue Sugar is sweet -Roses are red +Roses are red Violets are blue Sugar is sweet @@ -65,16 +66,26 @@ It is not reasonable to italicize just _part_ of a word, especially when you're perform_complicated_task do_this_and_do_that_and_another_thing -perform_complicated_task +perform_complicated_task do_this_and_do_that_and_another_thing -## URL autolinking +## URL auto-linking -GFM will autolink standard URLs you copy and paste into your text. So if you want to link to a URL (instead of a textural link), you can simply put the URL in verbatim and it will be turned into a link to that URL. +GFM will autolink almost any URL you copy and paste into your text. - http://www.google.com + * http://www.google.com + * https://google.com/ + * ftp://ftp.us.debian.org/debian/ + * smb://foo/bar/baz + * irc://irc.freenode.net/gitlab + * http://localhost:3000 -http://www.google.com +* http://www.google.com +* https://google.com/ +* ftp://ftp.us.debian.org/debian/ +* smb://foo/bar/baz +* irc://irc.freenode.net/gitlab +* http://localhost:3000 ## Code and Syntax Highlighting @@ -140,29 +151,29 @@ But let's throw in a <b>tag</b>. ## Emoji - Sometimes you want to be a :ninja: and add some :glowing_star: to your :speech_balloon:. Well we have a gift for you: + Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: - :high_voltage_sign: You can use emoji anywhere GFM is supported. :victory_hand: + :zap: You can use emoji anywhere GFM is supported. :v: - You can use it to point out a :bug: or warn about :speak_no_evil_monkey: patches. And if someone improves your really :snail: code, send them some :cake:. People will :heart: you for that. + You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that. - If you are new to this, don't be :fearful_face:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes. + If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes. - Consult the [Emoji Cheat Sheet](https://www.dropbox.com/s/b9xaqb977s6d8w1/cheat_sheet.pdf) for a list of all supported emoji codes. :thumbsup: + Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup: -Sometimes you want to be a :ninja: and add some :glowing_star: to your :speech_balloon:. Well we have a gift for you: +Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: -:high_voltage_sign: You can use emoji anywhere GFM is supported. :victory_hand: +:zap: You can use emoji anywhere GFM is supported. :v: -You can use it to point out a :bug: or warn about :speak_no_evil_monkey: patches. And if someone improves your really :snail: code, send them some :cake:. People will :heart: you for that. +You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that. -If you are new to this, don't be :fearful_face:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes. +If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes. -Consult the [Emoji Cheat Sheet](https://www.dropbox.com/s/b9xaqb977s6d8w1/cheat_sheet.pdf) for a list of all supported emoji codes. :thumbsup: +Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup: ## Special GitLab References -GFM recognized special references. +GFM recognizes special references. You can easily reference e.g. an issue, a commit, a team member or even the whole team within a project. @@ -170,31 +181,50 @@ GFM will turn that reference into a link so you can navigate between them easily GFM will recognize the following: -- @foo : for team members -- @all : for the whole team -- #123 : for issues -- !123 : for merge requests -- $123 : for snippets -- 1234567 : for commits -- \[file\](path/to/file) : for file references - -GFM also recognizes references to commits, issues, and merge requests in other projects: - -- namespace/project#123 : for issues -- namespace/project!123 : for merge requests -- namespace/project@1234567 : for commits +| 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 | + +GFM also recognizes certain cross-project references: + +| input | references | +|:----------------------------------------|:------------------------| +| `namespace/project#123` | issue | +| `namespace/project!123` | merge request | +| `namespace/project$123` | snippet | +| `namespace/project@9ba12248` | specific commit | +| `namespace/project@9ba12248...b19a04f5` | commit range comparison | ## Task Lists -You can add task lists to merge request and issue descriptions to keep track of to-do items. To create a task, add an unordered list to the description in an issue or merge request, formatted like so: +You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so: ```no-highlight -* [x] Completed task -* [ ] Unfinished task - * [x] Nested task +- [x] Completed task +- [ ] Incomplete task + - [ ] Sub-task 1 + - [x] Sub-task 2 + - [ ] Sub-task 3 ``` -Task lists can only be created in descriptions, not in titles or comments. Task item state can be managed by editing the description's Markdown or by clicking the rendered checkboxes. +- [x] Completed task +- [ ] Incomplete task + - [ ] Sub-task 1 + - [x] Sub-task 2 + - [ ] Sub-task 3 + +Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes. # Standard Markdown @@ -234,51 +264,38 @@ Alt-H2 ### Header IDs and links -All markdown rendered headers automatically get IDs, except for comments. +All Markdown-rendered headers automatically get IDs, except in comments. On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else. The IDs are generated from the content of the header according to the following rules: -1. remove the heading hashes `#` and process the rest of the line as it would be processed if it were not a header -2. from the result, remove all HTML tags, but keep their inner content -3. convert all characters to lowercase -4. convert all characters except `[a-z0-9_-]` into hyphens `-` -5. transform multiple adjacent hyphens into a single hyphen -6. remove trailing and heading hyphens +1. All text is converted to lowercase +1. All non-word text (e.g., punctuation, HTML) is removed +1. All spaces are converted to hyphens +1. Two or more hyphens in a row are converted to one +1. If a header with the same ID has already been generated, a unique + incrementing number is appended. For example: ``` -###### ..Ab_c-d. e [anchor](url) .. -``` - -which renders as: - -###### ..Ab_c-d. e [anchor](url) .. - -will first be converted by step 1) into a string like: - -``` -..Ab_c-d. e <a href="url">anchor</a> <img src="url" alt="alt text"/>.. -``` - -After removing the tags in step 2) we get: - -``` -..Ab_c-d. e anchor .. +# This header has spaces in it +## This header has a :thumbsup: in it +# This header has Unicode in it: 한글 +## This header has spaces in it +### This header has spaces in it ``` -And applying all the other steps gives the id: +Would generate the following link IDs: -``` -ab_c-d-e-anchor -``` +1. `this-header-has-spaces-in-it` +1. `this-header-has-a-in-it` +1. `this-header-has-unicode-in-it-한글` +1. `this-header-has-spaces-in-it-1` +1. `this-header-has-spaces-in-it-2` -Note in particular how: - -- for markdown anchors `[text](url)`, only the `text` is used -- markdown images `` are completely ignored +Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID. ## Emphasis @@ -310,8 +327,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~ 1. Ordered sub-list 4. And another item. - Some text that should be aligned with the above item. - * Unordered list can use asterisks - Or minuses + Or pluses @@ -324,8 +339,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~ 1. Ordered sub-list 4. And another item. - Some text that should be aligned with the above item. - * Unordered list can use asterisks - Or minuses + Or pluses @@ -420,6 +433,8 @@ Quote break. You can also use raw HTML in your Markdown, and it'll mostly work pretty well. +See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements. + ```no-highlight <dl> <dt>Definition list</dt> @@ -483,6 +498,10 @@ This line is separated from the one above by two newlines, so it will be a *sepa This line is also a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the *same paragraph*. + +This line is also a separate paragraph, and... +This line is on its own line, because the previous line ends with two +spaces. ``` Here's a line for us to start with. @@ -492,6 +511,10 @@ This line is separated from the one above by two newlines, so it will be a *sepa This line is also begins a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the *same paragraph*. +This line is also a separate paragraph, and... +This line is on its own line, because the previous line ends with two +spaces. + ## Tables Tables aren't part of the core Markdown spec, but they are part of GFM and Markdown Here supports them. @@ -514,6 +537,20 @@ Code above produces next output: The row of dashes between the table header and body must have at least three dashes in each column. +By including colons in the header row, you can align the text within that column: + +``` +| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned | +| :----------- | :------: | ------------: | :----------- | :------: | ------------: | +| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 | +| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 | +``` + +| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned | +| :----------- | :------: | ------------: | :----------- | :------: | ------------: | +| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 | +| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 | + ## References - This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). diff --git a/doc/operations/README.md b/doc/operations/README.md new file mode 100644 index 00000000000..6a35dab7b6c --- /dev/null +++ b/doc/operations/README.md @@ -0,0 +1,5 @@ +# GitLab operations + +- [Sidekiq MemoryKiller](sidekiq_memory_killer.md) +- [Cleaning up Redis sessions](cleaning_up_redis_sessions.md) +- [Understanding Unicorn and unicorn-worker-killer](unicorn.md) diff --git a/doc/operations/cleaning_up_redis_sessions.md b/doc/operations/cleaning_up_redis_sessions.md new file mode 100644 index 00000000000..93521e976d5 --- /dev/null +++ b/doc/operations/cleaning_up_redis_sessions.md @@ -0,0 +1,52 @@ +# Cleaning up stale Redis sessions + +Since version 6.2, GitLab stores web user sessions as key-value pairs in Redis. +Prior to GitLab 7.3, user sessions did not automatically expire from Redis. If +you have been running a large GitLab server (thousands of users) since before +GitLab 7.3 we recommend cleaning up stale sessions to compact the Redis +database after you upgrade to GitLab 7.3. You can also perform a cleanup while +still running GitLab 7.2 or older, but in that case new stale sessions will +start building up again after you clean up. + +In GitLab versions prior to 7.3.0, the session keys in Redis are 16-byte +hexadecimal values such as '976aa289e2189b17d7ef525a6702ace9'. Starting with +GitLab 7.3.0, the keys are +prefixed with 'session:gitlab:', so they would look like +'session:gitlab:976aa289e2189b17d7ef525a6702ace9'. Below we describe how to +remove the keys in the old format. + +First we define a shell function with the proper Redis connection details. + +``` +rcli() { + # This example works for Omnibus installations of GitLab 7.3 or newer. For an + # installation from source you will have to change the socket path and the + # path to redis-cli. + sudo /opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.socket "$@" +} + +# test the new shell function; the response should be PONG +rcli ping +``` + +Now we do a search to see if there are any session keys in the old format for +us to clean up. + +``` +# returns the number of old-format session keys in Redis +rcli keys '*' | grep '^[a-f0-9]\{32\}$' | wc -l +``` + +If the number is larger than zero, you can proceed to expire the keys from +Redis. If the number is zero there is nothing to clean up. + +``` +# Tell Redis to expire each matched key after 600 seconds. +rcli keys '*' | grep '^[a-f0-9]\{32\}$' | awk '{ print "expire", $0, 600 }' | rcli +# This will print '(integer) 1' for each key that gets expired. +``` + +Over the next 15 minutes (10 minutes expiry time plus 5 minutes Redis +background save interval) your Redis database will be compacted. If you are +still using GitLab 7.2, users who are not clicking around in GitLab during the +10 minute expiry window will be signed out of GitLab. diff --git a/doc/operations/sidekiq_memory_killer.md b/doc/operations/sidekiq_memory_killer.md new file mode 100644 index 00000000000..811c2192a19 --- /dev/null +++ b/doc/operations/sidekiq_memory_killer.md @@ -0,0 +1,40 @@ +# Sidekiq MemoryKiller + +The GitLab Rails application code suffers from memory leaks. For web requests +this problem is made manageable using +[unicorn-worker-killer](https://github.com/kzk/unicorn-worker-killer) which +restarts Unicorn worker processes in between requests when needed. The Sidekiq +MemoryKiller applies the same approach to the Sidekiq processes used by GitLab +to process background jobs. + +Unlike unicorn-worker-killer, which is enabled by default for all GitLab +installations since GitLab 6.4, the Sidekiq MemoryKiller is enabled by default +_only_ for Omnibus packages. The reason for this is that the MemoryKiller +relies on Runit to restart Sidekiq after a memory-induced shutdown and GitLab +installations from source do not all use Runit or an equivalent. + +With the default settings, the MemoryKiller will cause a Sidekiq restart no +more often than once every 15 minutes, with the restart causing about one +minute of delay for incoming background jobs. + +## Configuring the MemoryKiller + +The MemoryKiller is controlled using environment variables. + +- `SIDEKIQ_MEMORY_KILLER_MAX_RSS`: if this variable is set, and its value is + greater than 0, then after each Sidekiq job, the MemoryKiller will check the + RSS of the Sidekiq process that executed the job. If the RSS of the Sidekiq + process (expressed in kilobytes) exceeds SIDEKIQ_MEMORY_KILLER_MAX_RSS, a + delayed shutdown is triggered. The default value for Omnibus packages is set + [in the omnibus-gitlab + repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb). +- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults 900 seconds (15 minutes). When + a shutdown is triggered, the Sidekiq process will keep working normally for + another 15 minutes. +- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. When the grace + time has expired, the MemoryKiller tells Sidekiq to stop accepting new jobs. + Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells + Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must + restart Sidekiq. +- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to 'SIGTERM'. The name of + the final signal sent to the Sidekiq process when we want it to shut down. diff --git a/doc/operations/unicorn.md b/doc/operations/unicorn.md new file mode 100644 index 00000000000..31b432cd411 --- /dev/null +++ b/doc/operations/unicorn.md @@ -0,0 +1,86 @@ +# Understanding Unicorn and unicorn-worker-killer + +## Unicorn + +GitLab uses [Unicorn](http://unicorn.bogomips.org/), a pre-forking Ruby web +server, to handle web requests (web browsers and Git HTTP clients). Unicorn is +a daemon written in Ruby and C that can load and run a Ruby on Rails +application; in our case the Rails application is GitLab Community Edition or +GitLab Enterprise Edition. + +Unicorn has a multi-process architecture to make better use of available CPU +cores (processes can run on different cores) and to have stronger fault +tolerance (most failures stay isolated in only one process and cannot take down +GitLab entirely). On startup, the Unicorn 'master' process loads a clean Ruby +environment with the GitLab application code, and then spawns 'workers' which +inherit this clean initial environment. The 'master' never handles any +requests, that is left to the workers. The operating system network stack +queues incoming requests and distributes them among the workers. + +In a perfect world, the master would spawn its pool of workers once, and then +the workers handle incoming web requests one after another until the end of +time. In reality, worker processes can crash or time out: if the master notices +that a worker takes too long to handle a request it will terminate the worker +process with SIGKILL ('kill -9'). No matter how the worker process ended, the +master process will replace it with a new 'clean' process again. Unicorn is +designed to be able to replace 'crashed' workers without dropping user +requests. + +This is what a Unicorn worker timeout looks like in `unicorn_stderr.log`. The +master process has PID 56227 below. + +``` +[2015-06-05T10:58:08.660325 #56227] ERROR -- : worker=10 PID:53009 timeout (61s > 60s), killing +[2015-06-05T10:58:08.699360 #56227] ERROR -- : reaped #<Process::Status: pid 53009 SIGKILL (signal 9)> worker=10 +[2015-06-05T10:58:08.708141 #62538] INFO -- : worker=10 spawned pid=62538 +[2015-06-05T10:58:08.708824 #62538] INFO -- : worker=10 ready +``` + +### Tunables + +The main tunables for Unicorn are the number of worker processes and the +request timeout after which the Unicorn master terminates a worker process. +See the [omnibus-gitlab Unicorn settings +documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md) +if you want to adjust these settings. + +## unicorn-worker-killer + +GitLab has memory leaks. These memory leaks manifest themselves in long-running +processes, such as Unicorn workers. (The Unicorn master process is not known to +leak memory, probably because it does not handle user requests.) + +To make these memory leaks manageable, GitLab comes with the +[unicorn-worker-killer gem](https://github.com/kzk/unicorn-worker-killer). This +gem [monkey-patches](http://en.wikipedia.org/wiki/Monkey_patch) the Unicorn +workers to do a memory self-check after every 16 requests. If the memory of the +Unicorn worker exceeds a pre-set limit then the worker process exits. The +Unicorn master then automatically replaces the worker process. + +This is a robust way to handle memory leaks: Unicorn is designed to handle +workers that 'crash' so no user requests will be dropped. The +unicorn-worker-killer gem is designed to only terminate a worker process _in +between requests_, so no user requests are affected. + +This is what a Unicorn worker memory restart looks like in unicorn_stderr.log. +You see that worker 4 (PID 125918) is inspecting itself and decides to exit. +The threshold memory value was 254802235 bytes, about 250MB. With GitLab this +threshold is a random value between 200 and 250 MB. The master process (PID +117565) then reaps the worker process and spawns a new 'worker 4' with PID +127549. + +``` +[2015-06-05T12:07:41.828374 #125918] WARN -- : #<Unicorn::HttpServer:0x00000002734770>: worker (pid: 125918) exceeds memory limit (256413696 bytes > 254802235 bytes) +[2015-06-05T12:07:41.828472 #125918] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 125918) alive: 23 sec (trial 1) +[2015-06-05T12:07:42.025916 #117565] INFO -- : reaped #<Process::Status: pid 125918 exit 0> worker=4 +[2015-06-05T12:07:42.034527 #127549] INFO -- : worker=4 spawned pid=127549 +[2015-06-05T12:07:42.035217 #127549] INFO -- : worker=4 ready +``` + +One other thing that stands out in the log snippet above, taken from +Gitlab.com, is that 'worker 4' was serving requests for only 23 seconds. This +is a normal value for our current GitLab.com setup and traffic. + +The high frequency of Unicorn memory restarts on some GitLab sites can be a +source of confusion for administrators. Usually they are a [red +herring](http://en.wikipedia.org/wiki/Red_herring). diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index e21384d21dc..8cfa7f9c876 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -8,7 +8,6 @@ If a user is a GitLab administrator they receive all permissions. ## Project - | Action | Guest | Reporter | Developer | Master | Owner | |---------------------------------------|---------|------------|-------------|----------|--------| | Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ | @@ -29,6 +28,7 @@ If a user is a GitLab administrator they receive all permissions. | Add new team members | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ | | Enable/disable branch protection | | | | ✓ | ✓ | +| Turn on/off prot. branch push for devs| | | | ✓ | ✓ | | Rewrite/remove git tags | | | | ✓ | ✓ | | Edit project | | | | ✓ | ✓ | | Add deploy keys to project | | | | ✓ | ✓ | @@ -37,10 +37,15 @@ If a user is a GitLab administrator they receive all permissions. | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | | Force push to protected branches | | | | | | -| Remove protected branches | | | | | | +| Remove protected branches | | | | | | ## Group +In order for a group to appear as public and be browsable, it must contain at +least one public project. + +Any user can remove themselves from a group, unless they are the last Owner of the group. + | Action | Guest | Reporter | Developer | Master | Owner | |-------------------------|-------|----------|-----------|--------|-------| | Browse group | ✓ | ✓ | ✓ | ✓ | ✓ | @@ -48,5 +53,3 @@ If a user is a GitLab administrator they receive all permissions. | Create project in group | | | | ✓ | ✓ | | Manage group members | | | | | ✓ | | Remove group | | | | | ✓ | - -Any user can remove himself from a group, unless he is the last Owner of the group. diff --git a/doc/profile/2fa.png b/doc/profile/2fa.png Binary files differnew file mode 100644 index 00000000000..bbf415210d5 --- /dev/null +++ b/doc/profile/2fa.png diff --git a/doc/profile/2fa_auth.png b/doc/profile/2fa_auth.png Binary files differnew file mode 100644 index 00000000000..4a4fbe68984 --- /dev/null +++ b/doc/profile/2fa_auth.png diff --git a/doc/profile/README.md b/doc/profile/README.md new file mode 100644 index 00000000000..6f8359d87fa --- /dev/null +++ b/doc/profile/README.md @@ -0,0 +1,4 @@ +# Profile Settings + +- [Preferences](preferences.md) +- [Two-factor Authentication (2FA)](two_factor_authentication.md) diff --git a/doc/profile/preferences.md b/doc/profile/preferences.md new file mode 100644 index 00000000000..ce5f1936782 --- /dev/null +++ b/doc/profile/preferences.md @@ -0,0 +1,32 @@ +# Profile Preferences + +Settings in the **Profile > Preferences** page allow the user to customize +various aspects of the site to their liking. + +## Application theme + +Changing this setting allows the user to customize the color scheme used for the +navigation bar on the left side of the screen. + +The default is **Charcoal**. + +## Syntax highlighting theme + +Changing this setting allows the user to customize the theme used when viewing +syntax highlighted code on the site. + +The default is **White**. + +## Behavior + +### Default Dashboard + +For users who have access to a large number of projects but only keep up with a +select few, the amount of activity on the default Dashboard page can be +overwhelming. + +Changing this setting allows the user to redefine what their default dashboard +will be. Setting it to **Starred Projects** will make that Dashboard view the +default when signing in or clicking the application logo in the upper left. + +The default is **Your Projects**. diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md new file mode 100644 index 00000000000..fb215c8b269 --- /dev/null +++ b/doc/profile/two_factor_authentication.md @@ -0,0 +1,67 @@ +# Two-factor Authentication (2FA) + +Two-factor Authentication (2FA) provides an additional level of security to your +GitLab account. Once enabled, in addition to supplying your username and +password to login, you'll be prompted for a code generated by an application on +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. + +## Enabling 2FA + +**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**. + + + +**On your phone:** + +1. Install a compatible application. We recommend [Google Authenticator] +\(proprietary\) or [FreeOTP] \(open source\). +1. In the application, add a new entry in one of two ways: + * Scan the code with your phone's camera to add the entry automatically. + * Enter the details provided to add the entry manually. + +**In GitLab:** + +1. Enter the six-digit pin number from the entry on your phone into the **Pin + code** field. +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 +of recovery codes. + +## Recovery Codes + +Should you ever lose access to your phone, you can use one of the ten provided +backup codes to login to your account. We suggest copying or printing them for +storage in a safe place. **Each code can be used only once** to log in to your +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. + +## 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. + + + +## 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**. + +[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en +[FreeOTP]: https://fedorahosted.org/freeotp/ diff --git a/doc/project_services/hipchat.md b/doc/project_services/hipchat.md new file mode 100644 index 00000000000..021a93a288f --- /dev/null +++ b/doc/project_services/hipchat.md @@ -0,0 +1,54 @@ +# Atlassian HipChat + +GitLab provides a way to send HipChat notifications upon a number of events, +such as when a user pushes code, creates a branch or tag, adds a comment, and +creates a merge request. + +## Setup + +GitLab requires the use of a HipChat v2 API token to work. v1 tokens are +not supported at this time. Note the differences between v1 and v2 tokens: + +HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1 +token is allowed to send messages to *any* room. + +HipChat v2 API has tokens that are can be created using the Integrations tab +in the Group or Room admin page. By design, these are lightweight tokens that +allow GitLab to send messages only to *one* room. + +### Complete these steps in HipChat: + +1. Go to: https://admin.hipchat.com/admin +1. Click on "Group Admin" -> "Integrations". +1. Find "Build Your Own!" and click "Create". +1. Select the desired room, name the integration "GitLab", and click "Create". +1. In the "Send messages to this room by posting this URL" column, you should +see a URL in the format: + +``` + https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token> +``` + +HipChat is now ready to accept messages from GitLab. Next, set up the HipChat +service in GitLab. + +### Complete these steps in GitLab: + +1. Navigate to the project you want to configure for notifications. +1. Select "Settings" in the top navigation. +1. Select "Services" in the left navigation. +1. Click "HipChat". +1. Select the "Active" checkbox. +1. Insert the `token` field from the URL into the `Token` field on the Web page. +1. Insert the `room` field from the URL into the `Room` field on the Web page. +1. Save or optionally click "Test Settings". + +## Troubleshooting + +If you do not see notifications, make sure you are using a HipChat v2 API +token, not a v1 token. + +Note that the v2 token is tied to a specific room. If you want to be able to +specify arbitrary rooms, you can create an API token for a specific user in +HipChat under "Account settings" and "API access". Use the `XXX` value under +`auth_token=XXX`. diff --git a/doc/project_services/irker.md b/doc/project_services/irker.md new file mode 100644 index 00000000000..9875bebf66b --- /dev/null +++ b/doc/project_services/irker.md @@ -0,0 +1,46 @@ +# Irker IRC Gateway + +GitLab provides a way to push update messages to an Irker server. When +configured, pushes to a project will trigger the service to send data directly +to the Irker server. + +See the project homepage for further info: https://gitlab.com/esr/irker + +## Needed setup + +You will first need an Irker daemon. You can download the Irker code from its +gitorious repository on https://gitorious.org/irker: `git clone +git@gitorious.org:irker/irker.git`. Once you have downloaded the code, you can +run the python script named `irkerd`. This script is the gateway script, it acts +both as an IRC client, for sending messages to an IRC server obviously, and as a +TCP server, for receiving messages from the GitLab service. + +If the Irker server runs on the same machine, you are done. If not, you will +need to follow the firsts steps of the next section. + +## Optional setup + +In the `app/models/project_services/irker_service.rb` file, you can modify some +options in the `initialize_settings` method: +- **server_ip** (defaults to `localhost`): the server IP address where the +`irkerd` daemon runs; +- **server_port** (defaults to `6659`): the server port of the `irkerd` daemon; +- **max_channels** (defaults to `3`): the maximum number of recipients the +client is authorized to join, per project; +- **default_irc_uri** (no default) : if this option is set, it has to be in the +format `irc[s]://domain.name` and will be prepend to each and every channel +provided by the user which is not a full URI. + +If the Irker server and the GitLab application do not run on the same host, you +will **need** to setup at least the **server_ip** option. + +## Note on Irker recipients + +Irker accepts channel names of the form `chan` and `#chan`, both for the +`#chan` channel. If you want to send messages in query, you will need to add +`,isnick` avec the channel name, in this form: `Aorimn,isnick`. In this latter +case, `Aorimn` is treated as a nick and no more as a channel name. + +Irker can also join password-protected channels. Users need to append +`?key=thesecretpassword` to the chan name. + diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 20a69a211dd..03937d20728 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -4,15 +4,17 @@ __Project integrations with external services for continuous integration and mor ## Services -- Assemblia -- [Atlassian Bamboo CI](bamboo.md) An Atlassian product for continous integration. +- Assembla +- [Atlassian Bamboo CI](bamboo.md) An Atlassian product for continuous integration. - Build box - Campfire - Emails on push - Flowdock - Gemnasium - GitLab CI -- Hipchat -- PivotalTracker +- [HipChat](hipchat.md) An Atlassian product for private group chat and instant messaging. +- [Irker](irker.md) An IRC gateway to receive messages on repository updates. +- Pivotal Tracker - Pushover - Slack +- TeamCity diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md index 4712c387021..bd439f7c6f3 100644 --- a/doc/public_access/public_access.md +++ b/doc/public_access/public_access.md @@ -41,4 +41,4 @@ When visiting the public page of an user, you will only see listed projects whic ## Restricting the use of public or internal projects -In [gitlab.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/dbd88d453b8e6c78a423fa7e692004b1db6ea069/config/gitlab.yml.example#L64) you can disable public projects or public and internal projects for the entire GitLab installation to prevent people making code public by accident. +In the Admin area under Settings you can disable public projects or public and internal projects for the entire GitLab installation to prevent people making code public by accident. The restricted visibility settings do not apply to admin users. diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index 9e2f697bca6..770b7a70fe0 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -1,5 +1,8 @@ +# Rake tasks + - [Backup restore](backup_restore.md) - [Cleanup](cleanup.md) +- [Features](features.md) - [Maintenance](maintenance.md) and self-checks - [User management](user_management.md) - [Web hooks](web_hooks.md) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 68e8a14f52f..ae2d465e0c1 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -9,14 +9,23 @@ This archive will be saved in backup_path (see `config/gitlab.yml`). The filename will be `[TIMESTAMP]_gitlab_backup.tar`. This timestamp can be used to restore an specific backup. You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. +If you are interested in GitLab CI backup please follow to the [CI backup documentation](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/raketasks/backup_restore.md)* + ``` # use this command if you've installed GitLab with the Omnibus package sudo gitlab-rake gitlab:backup:create -# if you've installed GitLab from source or using the cookbook +# if you've installed GitLab from source sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` +Also you can choose what should be backed up by adding environment variable SKIP. Available options: db, +uploads (attachments), repositories. Use a comma to specify several options at the same time. + +``` +sudo gitlab-rake gitlab:backup:create SKIP=db,uploads +``` + Example output: ``` @@ -137,17 +146,15 @@ with the name of your bucket: Please be informed that a backup does not store your configuration files. If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration). If you have a cookbook installation there should be a copy of your configuration in Chef. -If you have a manual installation please consider backing up your gitlab.yml file and any SSL keys and certificates. +If you have an installation from source, please consider backing up your `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). ## Restore a previously created backup You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. -``` -# Omnibus package installation -sudo gitlab-rake gitlab:backup:restore +### Installation from source -# installation from source or cookbook +``` bundle exec rake gitlab:backup:restore RAILS_ENV=production ``` @@ -155,6 +162,7 @@ Options: ``` BACKUP=timestamp_of_backup (required if more than one backup exists) +force=yes (do not ask if the authorized_keys file should get regenerated) ``` Example output: @@ -188,11 +196,45 @@ Restoring repositories: Deleting tmp directories...[DONE] ``` -## Configure cron to make daily backups +### Omnibus installations + +We will assume that you have installed GitLab from an omnibus package and run +`sudo gitlab-ctl reconfigure` at least once. + +First make sure your backup tar file is in `/var/opt/gitlab/backups`. + +```shell +sudo cp 1393513186_gitlab_backup.tar /var/opt/gitlab/backups/ +``` + +Next, restore the backup by running the restore command. You need to specify the +timestamp of the backup you are restoring. + +```shell +# Stop processes that are connected to the database +sudo gitlab-ctl stop unicorn +sudo gitlab-ctl stop sidekiq -For Omnibus package installations, see https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#scheduling-a-backup . +# This command will overwrite the contents of your GitLab database! +sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186 -For installation from source or cookbook: +# Start GitLab +sudo gitlab-ctl start + +# Create satellites +sudo gitlab-rake gitlab:satellites:create + +# Check GitLab +sudo gitlab-rake gitlab:check SANITIZE=true +``` + +If there is a GitLab version mismatch between your backup tar file and the installed +version of GitLab, the restore command will abort with an error. Install a package for +the [required version](https://www.gitlab.com/downloads/archives/) and try again. + +## Configure cron to make daily backups + +### For installation from source: ``` cd /home/git/gitlab sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups @@ -208,3 +250,56 @@ Add the following lines at the bottom: The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors. This is recommended to reduce cron spam. + +### For omnibus installations + +To schedule a cron job that backs up your repositories and GitLab metadata, use the root user: + +``` +sudo su - +crontab -e +``` + +There, add the following line to schedule the backup for everyday at 2 AM: + +``` +0 2 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1 +``` + +You may also want to set a limited lifetime for backups to prevent regular +backups using all your disk space. To do this add the following lines to +`/etc/gitlab/gitlab.rb` and reconfigure: + +``` +# limit backup lifetime to 7 days - 604800 seconds +gitlab_rails['backup_keep_time'] = 604800 +``` + +NOTE: This cron job does not [backup your omnibus-gitlab configuration](#backup-and-restore-omnibus-gitlab-configuration) or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). + +## Alternative backup strategies + +If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow. +In this case you can consider using filesystem snapshots as part of your backup strategy. + +Example: Amazon EBS + +> A GitLab server using omnibus-gitlab hosted on Amazon AWS. +> An EBS drive containing an ext4 filesystem is mounted at `/var/opt/gitlab`. +> In this case you could make an application backup by taking an EBS snapshot. +> The backup includes all repositories, uploads and Postgres data. + +Example: LVM snapshots + rsync + +> A GitLab server using omnibus-gitlab, with an LVM logical volume mounted at `/var/opt/gitlab`. +> Replicating the `/var/opt/gitlab` directory using rsync would not be reliable because too many files would change while rsync is running. +> Instead of rsync-ing `/var/opt/gitlab`, we create a temporary LVM snapshot, which we mount as a read-only filesystem at `/mnt/gitlab_backup`. +> Now we can have a longer running rsync job which will create a consistent replica on the remote server. +> The replica includes all repositories, uploads and Postgres data. + +If you are running GitLab on a virtualized server you can possibly also create VM snapshots of the entire GitLab server. +It is not uncommon however for a VM snapshot to require you to power down the server, so this approach is probably of limited practical use. + +### Note +This documentation is for GitLab CE. +We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com.
\ No newline at end of file diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md index 9e48f56c951..96d67f7b5d6 100644 --- a/doc/raketasks/cleanup.md +++ b/doc/raketasks/cleanup.md @@ -8,7 +8,7 @@ Remove namespaces(dirs) from `/home/git/repositories` if they don't exist in Git # omnibus-gitlab sudo gitlab-rake gitlab:cleanup:dirs -# installation from source or cookbook +# installation from source bundle exec rake gitlab:cleanup:dirs RAILS_ENV=production ``` @@ -18,6 +18,6 @@ Remove repositories (global only for now) from `/home/git/repositories` if they # omnibus-gitlab sudo gitlab-rake gitlab:cleanup:repos -# installation from source or cookbook +# installation from source bundle exec rake gitlab:cleanup:repos RAILS_ENV=production ``` diff --git a/doc/raketasks/features.md b/doc/raketasks/features.md index 99b3d5525b0..f9a46193547 100644 --- a/doc/raketasks/features.md +++ b/doc/raketasks/features.md @@ -6,7 +6,7 @@ This command will enable the namespaces feature introduced in v4.0. It will move Note: -- Because the **repository location will change**, you will need to **update all your git url's** to point to the new location. +- Because the **repository location will change**, you will need to **update all your git URLs** to point to the new location. - Username can be changed at [Profile / Account](/profile/account) **Example:** diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md index bb229e8acbb..8a38937062e 100644 --- a/doc/raketasks/import.md +++ b/doc/raketasks/import.md @@ -13,15 +13,32 @@ - For omnibus-gitlab, it is located at: `/var/opt/gitlab/git-data/repositories` by default, unless you changed it in the `/etc/gitlab/gitlab.rb` file. -- For manual installations, it is usually located at: `/home/git/repositories` or you can see where +- For installations from source, it is usually located at: `/home/git/repositories` or you can see where your repositories are located by looking at `config/gitlab.yml` under the `gitlab_shell => repos_path` entry. +New folder needs to have git user ownership and read/write/execute access for git user and its group: + +``` +sudo -u git mkdir /var/opt/gitlab/git-data/repositories/new_group +``` + +If you are using an installation from source, replace `/var/opt/gitlab/git-data` +with `/home/git`. + ### Copy your bare repositories inside this newly created folder: ``` -$ cp -r /old/git/foo.git/ /home/git/repositories/new_group/ +sudo cp -r /old/git/foo.git /var/opt/gitlab/git-data/repositories/new_group/ + +# Do this once when you are done copying git repositories +sudo chown -R git:git /var/opt/gitlab/git-data/repositories/new_group/ ``` +`foo.git` needs to be owned by the git user and git users group. + +If you are using an installation from source, replace `/var/opt/gitlab/git-data` +with `/home/git`. + ### Run the command below depending on your type of installation: #### Omnibus Installation @@ -30,7 +47,7 @@ $ cp -r /old/git/foo.git/ /home/git/repositories/new_group/ $ sudo gitlab-rake gitlab:import:repos ``` -#### Manual Installation +#### Installation from source Before running this command you need to change the directory to where your GitLab installation is located: diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index 8bef92e55fe..69171cd1765 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -8,7 +8,7 @@ This command gathers information about your GitLab installation and the System i # omnibus-gitlab sudo gitlab-rake gitlab:env:info -# installation from source or cookbook +# installation from source bundle exec rake gitlab:env:info RAILS_ENV=production ``` @@ -16,37 +16,37 @@ Example output: ``` System information -System: Debian 6.0.7 -Current User: git -Using RVM: no -Ruby Version: 2.0.0-p481 -Gem Version: 1.8.23 -Bundler Version:1.3.5 -Rake Version: 10.0.4 +System: Debian 7.8 +Current User: git +Using RVM: no +Ruby Version: 2.1.5p273 +Gem Version: 2.4.3 +Bundler Version: 1.7.6 +Rake Version: 10.3.2 +Sidekiq Version: 2.17.8 GitLab information -Version: 5.1.0.beta2 -Revision: 4da8b37 -Directory: /home/git/gitlab -DB Adapter: mysql2 -URL: http://example.com -HTTP Clone URL: http://example.com/some-project.git -SSH Clone URL: git@example.com:some-project.git -Using LDAP: no -Using Omniauth: no +Version: 7.7.1 +Revision: 41ab9e1 +Directory: /home/git/gitlab +DB Adapter: postgresql +URL: https://gitlab.example.com +HTTP Clone URL: https://gitlab.example.com/some-project.git +SSH Clone URL: git@gitlab.example.com:some-project.git +Using LDAP: no +Using Omniauth: no GitLab Shell -Version: 1.2.0 -Repositories: /home/git/repositories/ -Hooks: /home/git/gitlab-shell/hooks/ -Git: /usr/bin/git +Version: 2.4.1 +Repositories: /home/git/repositories/ +Hooks: /home/git/gitlab-shell/hooks/ +Git: /usr/bin/git ``` ## Check GitLab configuration Runs the following rake tasks: -- `gitlab:env:check` - `gitlab:gitlab_shell:check` - `gitlab:sidekiq:check` - `gitlab:app:check` @@ -59,7 +59,7 @@ You may also have a look at our [Trouble Shooting Guide](https://github.com/gitl # omnibus-gitlab sudo gitlab-rake gitlab:check -# installation from source or cookbook +# installation from source bundle exec rake gitlab:check RAILS_ENV=production ``` @@ -127,7 +127,6 @@ sudo chmod u+rwx,g=rx,o-rwx /home/git/gitlab-satellites In some case it is necessary to rebuild the `authorized_keys` file. - For Omnibus-packages: ``` sudo gitlab-rake gitlab:shell:setup @@ -143,6 +142,41 @@ sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production This will rebuild an authorized_keys file. You will lose any data stored in authorized_keys file. Do you want to continue (yes/no)? yes +``` + +## Clear redis cache + +If for some reason the dashboard shows wrong information you might want to +clear Redis' cache. + +For Omnibus-packages: +``` +sudo gitlab-rake cache:clear +``` + +For installations from source: +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +## Precompile the assets + +Sometimes during version upgrades you might end up with some wrong CSS or +missing some icons. In that case, try to precompile the assets again. -............................ +Note that this only applies to source installations and does NOT apply to +omnibus packages. + +For installations from source: ``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production +``` + +For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at +the release of upstream GitLab. The omnibus version includes optimized versions +of those assets. Unless you are modifying the JavaScript / CSS code on your +production machine after installing the package, there should be no reason to redo +rake assets:precompile on the production machine. If you suspect that assets +have been corrupted, you should reinstall the omnibus package. diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md index 3c67753ad28..4fbd20762da 100644 --- a/doc/raketasks/user_management.md +++ b/doc/raketasks/user_management.md @@ -6,7 +6,7 @@ # omnibus-gitlab sudo gitlab-rake gitlab:import:user_to_projects[username@domain.tld] -# installation from source or cookbook +# installation from source bundle exec rake gitlab:import:user_to_projects[username@domain.tld] RAILS_ENV=production ``` @@ -20,7 +20,7 @@ Notes: # omnibus-gitlab sudo gitlab-rake gitlab:import:all_users_to_all_projects -# installation from source or cookbook +# installation from source bundle exec rake gitlab:import:all_users_to_all_projects RAILS_ENV=production ``` @@ -30,7 +30,7 @@ bundle exec rake gitlab:import:all_users_to_all_projects RAILS_ENV=production # omnibus-gitlab sudo gitlab-rake gitlab:import:user_to_groups[username@domain.tld] -# installation from source or cookbook +# installation from source bundle exec rake gitlab:import:user_to_groups[username@domain.tld] RAILS_ENV=production ``` @@ -44,6 +44,15 @@ Notes: # omnibus-gitlab sudo gitlab-rake gitlab:import:all_users_to_all_groups -# installation from source or cookbook +# installation from source bundle exec rake gitlab:import:all_users_to_all_groups RAILS_ENV=production ``` + +## Maintain tight control over the number of active users on your GitLab installation + +- Enable this setting to keep new users blocked until they have been cleared by the admin (default: false). + + +``` +block_auto_created_users: false +``` diff --git a/doc/raketasks/web_hooks.md b/doc/raketasks/web_hooks.md index e1a58835d88..5a8b94af9b4 100644 --- a/doc/raketasks/web_hooks.md +++ b/doc/raketasks/web_hooks.md @@ -4,42 +4,42 @@ # omnibus-gitlab sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook" - # source installations or cookbook + # source installations bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" RAILS_ENV=production ## Add a web hook for projects in a given **NAMESPACE**: # omnibus-gitlab sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acme - # source installations or cookbook + # source installations bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production ## Remove a web hook from **ALL** projects using: # omnibus-gitlab sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook" - # source installations or cookbook + # source installations bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" RAILS_ENV=production ## Remove a web hook from projects in a given **NAMESPACE**: # omnibus-gitlab sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme - # source installations or cookbook + # source installations bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production ## List **ALL** web hooks: # omnibus-gitlab sudo gitlab-rake gitlab:web_hook:list - # source installations or cookbook + # source installations bundle exec rake gitlab:web_hook:list RAILS_ENV=production ## List the web hooks from projects in a given **NAMESPACE**: # omnibus-gitlab sudo gitlab-rake gitlab:web_hook:list NAMESPACE=/ - # source installations or cookbook + # source installations bundle exec rake gitlab:web_hook:list NAMESPACE=/ RAILS_ENV=production > Note: `/` is the global namespace. diff --git a/doc/release/howto_rc1.md b/doc/release/howto_rc1.md new file mode 100644 index 00000000000..07c703142d4 --- /dev/null +++ b/doc/release/howto_rc1.md @@ -0,0 +1,55 @@ +# How to create RC1 + +The RC1 release comes with the task to update the installation and upgrade docs. Be mindful that there might already be merge requests for this on GitLab or GitHub. + +### 1. Update the installation guide + +1. Check if it references the correct branch `x-x-stable` (doesn't exist yet, but that is okay) +1. Check the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782) +1. Check the [Git version](/lib/tasks/gitlab/check.rake#L794) +1. There might be other changes. Ask around. + +### 2. Create update guides + +[Follow this guide](howto_update_guides.md) to create update guides. + +### 3. Code quality indicators + +Make sure the code quality indicators are green / good. + +- [](http://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch) + +- [](https://semaphoreapp.com/gitlabhq/gitlabhq) (master branch) + +- [](https://codeclimate.com/github/gitlabhq/gitlabhq) + +- [](https://gemnasium.com/gitlabhq/gitlabhq) this button can be yellow (small updates are available) but must not be red (a security fix or an important update is available) + +- [](https://coveralls.io/r/gitlabhq/gitlabhq) + +### 4. Run release tool + +**Make sure EE `master` has latest changes from CE `master`** + +Get release tools + +``` +git clone git@dev.gitlab.org:gitlab/release-tools.git +cd release-tools +``` + +Release candidate creates stable branch from master. +So we need to sync master branch between all CE, EE and CI remotes. + +``` +bundle exec rake sync +``` + +Create release candidate and stable branch: + +``` +bundle exec rake release["x.x.0.rc1"] +``` + +Now developers can use master for merging new features. +So you should use stable branch for future code changes related to release. diff --git a/doc/release/howto_update_guides.md b/doc/release/howto_update_guides.md new file mode 100644 index 00000000000..23d0959c33d --- /dev/null +++ b/doc/release/howto_update_guides.md @@ -0,0 +1,55 @@ +# Create update guides + +1. Create: CE update guide from previous version. Like `7.3-to-7.4.md` +1. Create: CE to EE update guide in EE repository for latest version. +1. Update: `6.x-or-7.x-to-7.x.md` to latest version. +1. Create: CI update guide from previous version + +It's best to copy paste the previous guide and make changes where necessary. +The typical steps are listed below with any points you should specifically look at. + +#### 0. Any major changes? + +List any major changes here, so the user is aware of them before starting to upgrade. For instance: + +- Database updates +- Web server changes +- File structure changes + +#### 1. Stop server + +#### 2. Make backup + +#### 3. Do users need to update dependencies like `git`? + +- Check if the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782) changed since the last release. + +- Check if the [Git version](/lib/tasks/gitlab/check.rake#L794) changed since the last release. + +#### 4. Get latest code + +#### 5. Does GitLab shell need to be updated? + +#### 6. Install libs, migrations, etc. + +#### 7. Any config files updated since last release? + +Check if any of these changed since last release: + +- [lib/support/nginx/gitlab](/lib/support/nginx/gitlab) +- [lib/support/nginx/gitlab-ssl](/lib/support/nginx/gitlab-ssl) +- <https://gitlab.com/gitlab-org/gitlab-shell/commits/master/config.yml.example> +- [config/gitlab.yml.example](/config/gitlab.yml.example) +- [config/unicorn.rb.example](/config/unicorn.rb.example) +- [config/database.yml.mysql](/config/database.yml.mysql) +- [config/database.yml.postgresql](/config/database.yml.postgresql) +- [config/initializers/rack_attack.rb.example](/config/initializers/rack_attack.rb.example) +- [config/resque.yml.example](/config/resque.yml.example) + +#### 8. Need to update init script? + +Check if the `init.d/gitlab` script changed since last release: [lib/support/init.d/gitlab](/lib/support/init.d/gitlab) + +#### 9. Start application + +#### 10. Check application status diff --git a/doc/release/master.md b/doc/release/master.md index 19070b46a0d..9163e652003 100644 --- a/doc/release/master.md +++ b/doc/release/master.md @@ -31,3 +31,32 @@ git remote add gl git@gitlab.com:gitlab-org/gitlab-ce.git gpa ``` +# Yanking packages from packages.gitlab.com + +In case something went wrong with the release and there is a need to remove the packages you can yank the packages by following the +procedure described in [package cloud documentation](https://packagecloud.io/docs#yank_pkg). + +You need to have: + +1. `package_cloud` gem installed (sudo gem install package_cloud) +1. Email and password for packages.gitlab.com +1. Make sure that you are supplying the url to packages.gitlab.com (default is packagecloud.io) + +Example of yanking a package: + +```bash +package_cloud yank --url https://packages.gitlab.com gitlab/gitlab-ce/el/6 gitlab-ce-7.10.2~omnibus-1.x86_64.rpm +``` + +If you are attempting this for the first time the output will look something like: + +```bash +Looking for repository at gitlab/gitlab-ce... No config file exists at /Users/marin/.packagecloud. Login to create one. +Email: +marin@gitlab.com +Password: + +Got your token. Writing a config file to /Users/marin/.packagecloud... success! +success! +Attempting to yank package at gitlab/gitlab-ce/el/6/gitlab-ce-7.10.2~omnibus-1.x86_64.rpm...done! +``` diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 64a8bc98344..7fb22938690 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -1,206 +1,129 @@ # Monthly Release -NOTE: This is a guide for GitLab developers. +NOTE: This is a guide used by the GitLab B.V. developers. -# **7 workdays before release - Code Freeze & Release Manager** +It starts 7 working days before the release. +The release manager doesn't have to perform all the work but must ensure someone is assigned. +The current release manager must schedule the appointment of the next release manager. +The new release manager should create overall issue to track the progress. -### **1. Stop merging in code, except for important bug fixes** +## Release Manager -### **2. Release Manager** - -A release manager is selected that coordinates all releases the coming month, including the patch releases for previous releases. +A release manager is selected that coordinates all releases the coming month, +including the patch releases for previous releases. The release manager has to make sure all the steps below are done and delegated where necessary. This person should also make sure this document is kept up to date and issues are created and updated. -### **3. Create an overall issue** - -Create issue for GitLab CE project(internal). Name it "Release x.x.x" for easier searching. -Replace the dates with actual dates based on the number of workdays before the release. - -``` -Xth: - -* Update the changelog (#LINK) -* Triage the omnibus-gitlab milestone - -Xth: - -* Merge CE in to EE (#LINK) -* Close the omnibus-gitlab milestone - -Xth: +## Take vacations into account -* Create x.x.0.rc1 (#LINK) -* Build package for GitLab.com (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#build-a-package) +The time is measured in weekdays to compensate for weekends. +Do everything on time to prevent problems due to rush jobs or too little testing time. +Make sure that you take into account any vacations of maintainers. +If the release is falling behind immediately warn the team. -Xth: +## Create an overall issue and follow it -* Update GitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package) -* Regression issue and tweet about rc1 (#LINK) -* Start blog post (#LINK) - -Xth: - -* Do QA and fix anything coming out of it (#LINK) - -22nd: - -* Release CE and EE (#LINK) - -Xth: - -* * Deploy to GitLab.com (#LINK) +Create issue for GitLab CE project(internal). Name it "Release x.x.x" for easier searching. +Replace the dates with actual dates based on the number of workdays before the release. +All steps from issue template are explained below ``` +Xth: (7 working days before the 22nd) -### **4. Update changelog** - -Any changes not yet added to the changelog are added by lead developer and in that merge request the complete team is asked if there is anything missing. +- [ ] Triage the omnibus-gitlab milestone -### **5. Take weekend and vacations into account** +Xth: (6 working days before the 22nd) -Ensure that there is enough time to incorporate the findings of the release candidate, etc. +- [ ] Merge CE master in to EE master via merge request (#LINK) +- [ ] Determine QA person and notify this person +- [ ] Check the tasks in [how to rc1 guide](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/release/howto_rc1.md) and delegate tasks if necessary +- [ ] Create CE, EE, CI RC1 versions (#LINK) +- [ ] Build RC1 packages (EE first) (#LINK) -# **6 workdays before release- Merge the CE into EE** +Xth: (5 working days before the 22nd) -Do this via a merge request. +- [ ] Do QA and fix anything coming out of it (#LINK) +- [ ] Close the omnibus-gitlab milestone +- [ ] Prepare the blog post (#LINK) -# **5 workdays before release - Create RC1** +Xth: (4 working days before the 22nd) -The RC1 release comes with the task to update the installation and upgrade docs. Be mindful that there might already be merge requests for this on GitLab or GitHub. +- [ ] Update GitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package) +- [ ] Update ci.gitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package) +- [ ] Create regression issues (CE, CI) (#LINK) +- [ ] Tweet about rc1 (#LINK), proposed text: -### **1. Update the installation guide** +> GitLab x.x.0.rc1 is available https://packages.gitlab.com/gitlab/unstable Use at your own risk. Please link regressions issues from LINK_TO_REGRESSION_ISSUE -1. Check if it references the correct branch `x-x-stable` (doesn't exist yet, but that is okay) -1. Check the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782) -1. Check the [Git version](/lib/tasks/gitlab/check.rake#L794) -1. There might be other changes. Ask around. +Xth: (3 working days before the 22nd) -### **2. Create update guides** +- [ ] Merge CE stable branch into EE stable branch -1. Create: CE update guide from previous version. Like `7.3-to-7.4.md` -1. Create: CE to EE update guide in EE repository for latest version. -1. Update: `6.x-or-7.x-to-7.x.md` to latest version. +Xth: (2 working days before the 22nd) -It's best to copy paste the previous guide and make changes where necessary. -The typical steps are listed below with any points you should specifically look at. +- [ ] Check that everyone is mentioned on the blog post using `@all` (the reviewer should have done this one working day ago) +- [ ] Check that MVP is added to the mvp page (source/mvp/index.html in www-gitlab-com) -#### 0. Any major changes? +Xth: (1 working day before the 22nd) -List any major changes here, so the user is aware of them before starting to upgrade. For instance: +- [ ] Create CE, EE, CI stable versions (#LINK) +- [ ] Create Omnibus tags and build packages +- [ ] Update GitLab.com with the stable version (#LINK) +- [ ] Update ci.gitLab.com with the stable version (#LINK) -- Database updates -- Web server changes -- File structure changes +22nd before 12AM CET: -#### 1. Make backup +Release before 12AM CET / 3AM PST, to make sure the majority of our users +get the new version on the 22nd and there is sufficient time in the European +workday to quickly fix any issues. -#### 2. Stop server +- [ ] Release CE, EE and CI (#LINK) +- [ ] Schedule a second tweet of the release announcement at 6PM CET / 9AM PST -#### 3. Do users need to update dependencies like `git`? - -- Check if the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782) changed since the last release. - -- Check if the [Git version](/lib/tasks/gitlab/check.rake#L794) changed since the last release. - -#### 4. Get latest code - -#### 5. Does GitLab shell need to be updated? - -#### 6. Install libs, migrations, etc. - -#### 7. Any config files updated since last release? - -Check if any of these changed since last release: - -- [lib/support/nginx/gitlab](/lib/support/nginx/gitlab) -- [lib/support/nginx/gitlab-ssl](/lib/support/nginx/gitlab-ssl) -- <https://gitlab.com/gitlab-org/gitlab-shell/commits/master/config.yml.example> -- [config/gitlab.yml.example](/config/gitlab.yml.example) -- [config/unicorn.rb.example](/config/unicorn.rb.example) -- [config/database.yml.mysql](/config/database.yml.mysql) -- [config/database.yml.postgresql](/config/database.yml.postgresql) -- [config/initializers/rack_attack.rb.example](/config/initializers/rack_attack.rb.example) -- [config/resque.yml.example](/config/resque.yml.example) - -#### 8. Need to update init script? - -Check if the `init.d/gitlab` script changed since last release: [lib/support/init.d/gitlab](/lib/support/init.d/gitlab) - -#### 9. Start application - -#### 10. Check application status - -### **3. Code quality indicators** - -Make sure the code quality indicators are green / good. - -- [](http://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch) - -- [](https://semaphoreapp.com/gitlabhq/gitlabhq) (master branch) +``` -- [](https://codeclimate.com/github/gitlabhq/gitlabhq) +- - - -- [](https://gemnasium.com/gitlabhq/gitlabhq) this button can be yellow (small updates are available) but must not be red (a security fix or an important update is available) +## Update changelog -- [](https://coveralls.io/r/gitlabhq/gitlabhq) +Any changes not yet added to the changelog are added by lead developer and in that merge request the complete team is +asked if there is anything missing. -### **4. Run release tool** +There are three changelogs that need to be updated: CE, EE and CI. -**Make sure EE `master` has latest changes from CE `master`** +## Create RC1 (CE, EE, CI) -Get release tools +[Follow this How-to guide](howto_rc1.md) to create RC1. -``` -git clone git@dev.gitlab.org:gitlab/release-tools.git -cd release-tools -``` +## Prepare CHANGELOG for next release -Release candidate creates stable branch from master. -So we need to sync master branch between all CE remotes. Also do same for EE. +Once the stable branches have been created, update the CHANGELOG in `master` with the upcoming version, usually X.X.X.pre. -``` -bundle exec rake sync -``` +On creating the stable branches, notify the core team and developers. -Create release candidate and stable branch: +## QA -``` -bundle exec rake release["x.x.0.rc1"] -``` +Create issue on dev.gitlab.org `gitlab` repository, named "GitLab X.X QA" in order to keep track of the progress. -Now developers can use master for merging new features. -So you should use stable branch for future code chages related to release. +Use the omnibus packages created for RC1 of Enterprise Edition using [this guide](https://dev.gitlab.org/gitlab/gitlab-ee/blob/master/doc/release/manual_testing.md). +**NOTE** Upgrader can only be tested when tags are pushed to all repositories. Do not forget to confirm it is working before releasing. Note that in the issue. -# **4 workdays before release - Release RC1** +#### Fix anything coming out of the QA -### **1. Determine QA person +Create an issue with description of a problem, if it is quick fix fix it yourself otherwise contact the team for advice. -Notify person of QA day. +**NOTE** If there is a problem that cannot be fixed in a timely manner, reverting the feature is an option! If the feature is reverted, +create an issue about it in order to discuss the next steps after the release. -### **2. Update GitLab.com** +## Update GitLab.com with RC1 -Merge the RC1 EE code into GitLab.com. -Once the build is green, create a package. +Use the omnibus EE packages created for RC1. If there are big database migrations consider testing them with the production db on a VM. Try to deploy in the morning. It is important to do this as soon as possible, so we can catch any errors before we release the full version. -### **3. Prepare the blog post** - -- Start with a complete copy of the [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md) and fill it out. -- Check the changelog of CE and EE for important changes. -- Create a WIP MR for the blog post -- Ask Dmitriy to add screenshots to the WIP MR. -- Decide with team who will be the MVP user. -- Create WIP MR for adding MVP to MVP page on website -- Add a note if there are security fixes: This release fixes an important security issue and we advise everyone to upgrade as soon as possible. -- Create a merge request on [GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com/tree/master) -- Assign to one reviewer who will fix spelling issues by editing the branch (can use the online editor) -- After the reviewer is finished the whole team will be mentioned to give their suggestions via line comments - -### **4. Create a regressions issue** +## Create a regressions issue On [the GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues/) create an issue titled "GitLab X.X regressions" add the following text: @@ -209,41 +132,33 @@ Please do not raise issues directly in this issue but link to issues that might The decision to create a patch release or not is with the release manager who is assigned to this issue. The release manager will comment here about the plans for patch releases. -Assign the issue to the release manager and /cc all the core-team members active on the issue tracker. If there are any known bugs in the release add them immediately. +Assign the issue to the release manager and at mention all members of gitlab core team. If there are any known bugs in the release add them immediately. -### **4. Tweet** +## Tweet about RC1 Tweet about the RC release: > GitLab x.x.0.rc1 is out. This release candidate is only suitable for testing. Please link regressions issues from LINK_TO_REGRESSION_ISSUE -# **1 workdays before release - Preparation** - -### **1. Pre QA merge** - -Merge CE into EE before doing the QA. - -### **2. QA** - -Create issue on dev.gitlab.org `gitlab` repository, named "GitLab X.X QA" in order to keep track of the progress. - -Use the omnibus packages of Enterprise Edition using [this guide](https://dev.gitlab.org/gitlab/gitlab-ee/blob/master/doc/release/manual_testing.md). - -**NOTE** Upgrader can only be tested when tags are pushed to all repositories. Do not forget to confirm it is working before releasing. Note that in the issue. - -### **3. Fix anything coming out of the QA** - -Create an issue with description of a problem, if it is quick fix fix it yourself otherwise contact the team for advice. - -**NOTE** If there is a problem that cannot be fixed in a timely manner, reverting the feature is an option! If the feature is reverted, -create an issue about it in order to discuss the next steps after the release. - -# **22nd - Release CE and EE** - -**Make sure EE `x-x-stable-ee` has latest changes from CE `x-x-stable`** - - -### **1. Release code** +## Prepare the blog post + +1. The blog post template for this release should already exist and might have comments that were added during the month. +1. Fill out as much of the blog post template as you can. +1. Make sure the blog post contains information about the GitLab CI release. +1. Check the changelog of CE and EE for important changes. +1. Also check the CI changelog +1. Add a proposed tweet text to the blog post WIP MR description. +1. Create a WIP MR for the blog post +1. Ask Dmitriy (or a team member with OS X) to add screenshots to the WIP MR. +1. Decide with core team who will be the MVP user. +1. Create WIP MR for adding MVP to MVP page on website +1. Add a note if there are security fixes: This release fixes an important security issue and we advise everyone to upgrade as soon as possible. +1. Create a merge request on [GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com/tree/master) +1. Assign to one reviewer who will fix spelling issues by editing the branch (either with a git client or by using the online editor) +1. Comment to the reviewer: '@person Please mention the whole team as soon as you are done (3 workdays before release at the latest)' +1. Create a complete copy of the [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md) for the release after this. + +## Create CE, EE, CI stable versions Get release tools @@ -258,47 +173,45 @@ Bump version, create release tag and push to remotes: bundle exec rake release["x.x.0"] ``` - -### **2. Update installation.md** +This will create correct version and tag and push to all CE, EE and CI remotes. Update [installation.md](/doc/install/installation.md) to the newest version in master. -### **3. Build the Omnibus packages** +## Create Omnibus tags and build packages Follow the [release doc in the Omnibus repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md). This can happen before tagging because Omnibus uses tags in its own repo and SHA1's to refer to the GitLab codebase. +## Update GitLab.com with the stable version + +- Deploy the package (should not need downtime because of the small difference with RC1) +- Deploy the package for ci.gitlab.com -### **4. Publish packages for new release** +## Release CE, EE and CI + +__1. Publish packages for new release__ Update `downloads/index.html` and `downloads/archive/index.html` in `www-gitlab-com` repository. -### **5. Publish blog for new release** +__2. Publish blog for new release__ +Doublecheck the everyone has been mentioned in the blog post. Merge the [blog merge request](#1-prepare-the-blog-post) in `www-gitlab-com` repository. -### **6. Tweet to blog** +__3. Tweet to blog__ Send out a tweet to share the good news with the world. List the most important features and link to the blog post. -Proposed tweet for CE "GitLab X.X is released! It brings *** <link-to-blogpost>" - -# **1 workday after release - Update GitLab.com** +Proposed tweet "Release of GitLab X.X & CI Y.Y! FEATURE, FEATURE and FEATURE <link-to-blog-post> #gitlab" -- Build a package for gitlab.com based on the official release instead of RC1 -- Deploy the package +Consider creating a post on Hacker News. -# **25th - Release GitLab CI** +## Release new AMIs -- Create the update guid `doc/x.x-to-x.x.md`. -- Update CHANGELOG -- Bump version -- Create annotated tags `git tag -a vx.x.0 -m 'Version x.x.0' xxxxx` -- Create stable branch `x-x-stable` -- Create GitHub release post -- Post to blog about release -- Post to twitter +[Follow this guide](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) +## Create a WIP blogpost for the next release +Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). diff --git a/doc/release/patch.md b/doc/release/patch.md index ce5c2170302..a569bb3da8d 100644 --- a/doc/release/patch.md +++ b/doc/release/patch.md @@ -21,10 +21,12 @@ Otherwise include it in the monthly release and note there was a regression fix 1. Consider creating and testing workarounds 1. After the branch is merged into master, cherry pick the commit(s) into the current stable branch 1. Make sure that the build has passed and all tests are passing -1. In a separate commit in the stable branch update the CHANGELOG +1. In a separate commit in the master branch update the CHANGELOG 1. For EE, update the CHANGELOG-EE if it is EE specific fix. Otherwise, merge the stable CE branch and add to CHANGELOG-EE "Merge community edition changes for version X.X.X" +1. Merge CE stable branch into EE stable branch -### Bump version + +### Bump version Get release tools @@ -33,23 +35,22 @@ git clone git@dev.gitlab.org:gitlab/release-tools.git cd release-tools ``` -Bump version in stable branch, create release tag and push to remotes: - -``` -bundle exec rake release["x.x.x"] -``` +Bump all versions in stable branch, even if the changes affect only EE, CE, or CI. Since all the versions are synced now, +it doesn't make sense to say upgrade CE to 7.2, EE to 7.3 and CI to 7.1. -Or if you need to release only EE: +Create release tag and push to remotes: ``` -CE=false be rake release['x.x.x'] +bundle exec rake release["x.x.x"] ``` -### Release +## Release -1. Apply the patch to GitLab Cloud and the private GitLab development server 1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md) -1. Cherry-pick the changelog update back into master -1. Create and publish a blog post -1. Send tweets about the release from `@gitlabhq`, tweet should include the most important feature that the release is addressing and link to the blog post +1. Apply the patch to GitLab.com and the private GitLab development server +1. Apply the patch to ci.gitLab.com and the private GitLab CI development server +1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md) +1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post 1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only) +1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) +1. Create a new patch release issue for the next potential release
\ No newline at end of file diff --git a/doc/release/security.md b/doc/release/security.md index c24a394ef4a..60bcfbb6da5 100644 --- a/doc/release/security.md +++ b/doc/release/security.md @@ -17,11 +17,14 @@ Please report suspected security vulnerabilities in private to <support@gitlab.c 1. Inform the release manager that there needs to be a security release 1. Do the steps from [patch release document](doc/release/patch.md), starting with "Create an issue on private GitLab development server" 1. The MR with the security fix should get a 'security' label and be assigned to the release manager +1. Build the package for GitLab.com and do a deploy +1. Build the package for ci.gitLab.com and do a deploy +1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) 1. Create feature branches for the blog post on GitLab.com and link them from the code branch 1. Merge and publish the blog posts 1. Send tweets about the release from `@gitlabhq` 1. Send out an email to [the community google mailing list](https://groups.google.com/forum/#!forum/gitlabhq) -1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number +1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number. CVE is only needed for bugs that allow someone to own the server (Remote Code Execution) or access to code of projects they are not a member of. 1. Add the security researcher to the [Security Researcher Acknowledgments list](http://about.gitlab.com/vulnerability-acknowledgements/) 1. Thank the security researcher in an email for their cooperation 1. Update the blog post and the CHANGELOG when we receive the CVE number diff --git a/doc/security/README.md b/doc/security/README.md index f88375f2afd..473f3632dcd 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -2,4 +2,6 @@ - [Password length limits](password_length_limits.md) - [Rack attack](rack_attack.md) +- [Web Hooks and insecure internal web services](webhooks.md) - [Information exclusivity](information_exclusivity.md) +- [Reset your root password](reset_root_password.md)
\ No newline at end of file diff --git a/doc/security/information_exclusivity.md b/doc/security/information_exclusivity.md index 127166ae2e7..f8e7fc3fd0e 100644 --- a/doc/security/information_exclusivity.md +++ b/doc/security/information_exclusivity.md @@ -4,6 +4,6 @@ Git is a distributed version control system (DVCS). This means that everyone that works with the source code has a local copy of the complete repository. In GitLab every project member that is not a guest (so reporters, developers and masters) can clone the repository to get a local copy. After obtaining this local copy the user can upload the full repository anywhere, including another project under their control or another server. -The consequense is that you can't build access controls that prevent the intentional sharing of source code by users that have access to the source code. +The consequence is that you can't build access controls that prevent the intentional sharing of source code by users that have access to the source code. This is an inherent feature of a DVCS and all git management systems have this limitation. Obviously you can take steps to prevent unintentional sharing and information destruction, this is why only some people are allowed to invite others and nobody can force push a protected branch. diff --git a/doc/security/reset_root_password.md b/doc/security/reset_root_password.md new file mode 100644 index 00000000000..3c13f262677 --- /dev/null +++ b/doc/security/reset_root_password.md @@ -0,0 +1,40 @@ +# How to reset your root password + +Log into your server with root privileges. Then start a Ruby on Rails console. + +Start the console with this command: + +```bash +gitlab-rails console production +``` + +Wait until the console has loaded. + +There are multiple ways to find your user. You can search for email or username. + +```bash +user = User.where(id: 1).first +``` + +or + +```bash +user = User.find_by(email: 'admin@local.host') +``` + +Now you can change your password: + +```bash +user.password = 'secret_pass' +user.password_confirmation = 'secret_pass' +``` + +It's important that you change both password and password_confirmation to make it work. + +Don't forget to save the changes. + +```bash +user.save! +``` + +Exit the console and try to login with your new password.
\ No newline at end of file diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md new file mode 100644 index 00000000000..1e9d33e87c3 --- /dev/null +++ b/doc/security/webhooks.md @@ -0,0 +1,13 @@ +# Web Hooks and insecure internal web services + +If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Web Hooks. + +With [Web Hooks](../web_hooks/web_hooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way. + +Things get hairy, however, when a Web Hook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the web hook is triggered and the POST request is sent. + +Because Web Hook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world. + +If a web service does not require authentication, Web Hooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete". + +To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough.
\ No newline at end of file diff --git a/doc/ssh/README.md b/doc/ssh/README.md index c87fffd7d2c..5f44f9351dd 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -1,4 +1,107 @@ # SSH -- [Deploy keys](deploy_keys.md) -- [SSH](ssh.md) +## SSH keys + +An SSH key allows you to establish a secure connection between your +computer and GitLab. + +Before generating an SSH key, check if your system already has one by +running `cat ~/.ssh/id_rsa.pub`. If you see a long string starting with +`ssh-rsa` or `ssh-dsa`, you can skip the ssh-keygen step. + +To generate a new SSH key, just open your terminal and use code below. The +ssh-keygen command prompts you for a location and filename to store the key +pair and for a password. When prompted for the location and filename, you +can press enter to use the default. + +It is a best practice to use a password for an SSH key, but it is not +required and you can skip creating a password by pressing enter. Note that +the password you choose here can't be altered or retrieved. + +```bash +ssh-keygen -t rsa -C "$your_email" +``` + +Use the code below to show your public key. + +```bash +cat ~/.ssh/id_rsa.pub +``` + +Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your +user profile. Please copy the complete key starting with `ssh-` and ending +with your username and host. + +Use code below to copy your public key to the clipboard. Depending on your +OS you'll need to use a different command: + +**Windows:** +```bash +clip < ~/.ssh/id_rsa.pub +``` + +**Mac:** +```bash +pbcopy < ~/.ssh/id_rsa.pub +``` + +**GNU/Linux (requires xclip):** +```bash +xclip -sel clip < ~/.ssh/id_rsa.pub +``` + +## Deploy keys + +Deploy keys allow read-only access to multiple projects with a single SSH +key. + +This is really useful for cloning repositories to your Continuous +Integration (CI) server. By using deploy keys, you don't have to setup a +dummy user account. + +If you are a project master or owner, you can add a deploy key in the +project settings under the section 'Deploy Keys'. Press the 'New Deploy +Key' button and upload a public SSH key. After this, the machine that uses +the corresponding private key has read-only access to the project. + +You can't add the same deploy key twice with the 'New Deploy Key' option. +If you want to add the same key to another project, please enable it in the +list that says 'Deploy keys from projects available to you'. All the deploy +keys of all the projects you have access to are available. This project +access can happen through being a direct member of the project, or through +a group. See `def accessible_deploy_keys` in `app/models/user.rb` for more +information. + +## Applications + +### Eclipse + +How to add your ssh key to Eclipse: http://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration + +## Tip: Non-default OpenSSH key file names or locations + +If, for whatever reason, you decide to specify a non-default location and filename for your Gitlab SSH key pair, you must configure your SSH client to find your Gitlab SSH private key for connections to your Gitlab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following: + +``` +# +# Main gitlab.com server +# +Host gitlab.com +RSAAuthentication yes +IdentityFile ~/my-ssh-key-directory/my-gitlab-private-key-filename +User mygitlabusername +``` + +Another example +``` +# +# Our company's internal Gitlab server +# +Host my-gitlab.company.com +RSAAuthentication yes +IdentityFile ~/my-ssh-key-directory/company-com-private-key-filename +``` + +Note in the gitlab.com example above a username was specified to override the default chosen by OpenSSH (your local username). This is only required if your local and remote usernames differ. + +Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document. diff --git a/doc/ssh/deploy_keys.md b/doc/ssh/deploy_keys.md deleted file mode 100644 index dcca8bdc61a..00000000000 --- a/doc/ssh/deploy_keys.md +++ /dev/null @@ -1,9 +0,0 @@ -# Deploy keys - -Deploy keys allow read-only access one or multiple projects with a single SSH key. - -This is really useful for cloning repositories to your Continuous Integration (CI) server. By using a deploy keys you don't have to setup a dummy user account. - -If you are a project master or owner you can add a deploy key in the project settings under the section Deploy Keys. Press the 'New Deploy Key' button and upload a public ssh key. After this the machine that uses the corresponding private key has read-only access to the project. - -You can't add the same deploy key twice with the 'New Deploy Key' option. If you want to add the same key to another project please enable it in the list that says 'Deploy keys from projects available to you'. All the deploy keys of all the projects you have access to are available. This project access can happen through being a direct member of the project or through a group. See `def accessible_deploy_keys` in `app/models/user.rb` for more information. diff --git a/doc/ssh/ssh.md b/doc/ssh/ssh.md deleted file mode 100644 index d466c1bde72..00000000000 --- a/doc/ssh/ssh.md +++ /dev/null @@ -1,21 +0,0 @@ -# SSH keys - -SSH key allows you to establish a secure connection between your computer and GitLab - -Before generating an SSH key, check if your system already has one by running `cat ~/.ssh/id_rsa.pub` If your see a long string starting with `ssh-rsa` or `ssh-dsa`, you can skip the ssh-keygen step. - -To generate a new SSH key just open your terminal and use code below. The ssh-keygen command prompts you for a location and filename to store the key pair and for a password. When prompted for the location and filename you can press enter to use the default. -It is a best practice to use a password for an SSH key but it is not required and you can skip creating a password by pressing enter. -Note that the password you choose here can't be altered or retrieved. - -```bash -ssh-keygen -t rsa -C "$your_email" -``` - -Use the code below to show your public key. - -```bash -cat ~/.ssh/id_rsa.pub -``` - -Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your user profile. Please copy the complete key starting with `ssh-` and ending with your username and host. diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 54e6e3a9e3f..b0e4613cdef 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -1,11 +1,17 @@ # System hooks -Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create` and `key_destroy`. +Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`. System hooks can be used, e.g. for logging or changing information in a LDAP server. ## Hooks request example +**Request header**: + +``` +X-Gitlab-Event: System Hook +``` + **Project created:** ```json @@ -15,8 +21,8 @@ System hooks can be used, e.g. for logging or changing information in a LDAP ser "name": "StoreCloud", "owner_email": "johnsmith@gmail.com", "owner_name": "John Smith", - "path": "stormcloud", - "path_with_namespace": "jsmith/stormcloud", + "path": "storecloud", + "path_with_namespace": "jsmith/storecloud", "project_id": 74, "project_visibility": "private", } @@ -50,6 +56,7 @@ System hooks can be used, e.g. for logging or changing information in a LDAP ser "project_path": "storecloud", "user_email": "johnsmith@gmail.com", "user_name": "John Smith", + "user_id": 41, "project_visibility": "private", } ``` @@ -66,6 +73,7 @@ System hooks can be used, e.g. for logging or changing information in a LDAP ser "project_path": "storecloud", "user_email": "johnsmith@gmail.com", "user_name": "John Smith", + "user_id": 41, "project_visibility": "private", } ``` @@ -117,3 +125,62 @@ System hooks can be used, e.g. for logging or changing information in a LDAP ser "id": 4 } ``` + +**Group created:** + +```json +{ + "created_at": "2012-07-21T07:30:54Z", + "event_name": "group_create", + "name": "StoreCloud", + "owner_email": "johnsmith@gmail.com", + "owner_name": "John Smith", + "path": "storecloud", + "group_id": 78 +} +``` + +**Group removed:** + +```json +{ + "created_at": "2012-07-21T07:30:54Z", + "event_name": "group_destroy", + "name": "StoreCloud", + "owner_email": "johnsmith@gmail.com", + "owner_name": "John Smith", + "path": "storecloud", + "group_id": 78 +} +``` + +**New Group Member:** + +```json +{ + "created_at": "2012-07-21T07:30:56Z", + "event_name": "user_add_to_group", + "group_access": "Master", + "group_id": 78, + "group_name": "StoreCloud", + "group_path": "storecloud", + "user_email": "johnsmith@gmail.com", + "user_name": "John Smith", + "user_id": 41 +} +``` +**Group Member Removed:** + +```json +{ + "created_at": "2012-07-21T07:30:56Z", + "event_name": "user_remove_from_group", + "group_access": "Master", + "group_id": 78, + "group_name": "StoreCloud", + "group_path": "storecloud", + "user_email": "johnsmith@gmail.com", + "user_name": "John Smith", + "user_id": 41 +} +``` diff --git a/doc/update/2.6-to-3.0.md b/doc/update/2.6-to-3.0.md index 6aabbe095dc..4827ef9501a 100644 --- a/doc/update/2.6-to-3.0.md +++ b/doc/update/2.6-to-3.0.md @@ -1,4 +1,5 @@ # From 2.6 to 3.0 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/2.6-to-3.0.md) for the most up to date instructions.* ## 1. Stop server & resque @@ -22,29 +23,29 @@ sudo -u gitlab bundle exec rake db:migrate RAILS_ENV=production # !!! Config should be replaced with a new one. Check it after replace cp config/gitlab.yml.example config/gitlab.yml -# update gitolite hooks +# update Gitolite hooks -# GITOLITE v2: +# Gitolite v2: sudo cp ./lib/hooks/post-receive /home/git/share/gitolite/hooks/common/post-receive sudo chown git:git /home/git/share/gitolite/hooks/common/post-receive -# GITOLITE v3: +# Gitolite v3: sudo cp ./lib/hooks/post-receive /home/git/.gitolite/hooks/common/post-receive sudo chown git:git /home/git/.gitolite/hooks/common/post-receive # set valid path to hooks in gitlab.yml in git_host section # like this git_host: - # gitolite 2 + # Gitolite 2 hooks_path: /home/git/share/gitolite/hooks - # gitolite 3 + # Gitolite 3 hooks_path: /home/git/.gitolite/hooks/ -# Make some changes to gitolite config +# Make some changes to Gitolite config # For more information visit https://github.com/gitlabhq/gitlabhq/pull/1719 -# gitolite v2 +# Gitolite v2 sudo -u git -H sed -i 's/\(GL_GITCONFIG_KEYS\s*=>*\s*\).\{2\}/\\1"\.\*"/g' /home/git/.gitolite.rc # gitlite v3 diff --git a/doc/update/2.9-to-3.0.md b/doc/update/2.9-to-3.0.md index 8af86b0dc98..f4a997a8c5e 100644 --- a/doc/update/2.9-to-3.0.md +++ b/doc/update/2.9-to-3.0.md @@ -1,4 +1,5 @@ # From 2.9 to 3.0 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/2.9-to-3.0.md) for the most up to date instructions.* ## 1. Stop server & resque diff --git a/doc/update/3.0-to-3.1.md b/doc/update/3.0-to-3.1.md index 3206df3499b..a30485c42f7 100644 --- a/doc/update/3.0-to-3.1.md +++ b/doc/update/3.0-to-3.1.md @@ -1,4 +1,5 @@ # From 3.0 to 3.1 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/3.0-to-3.1.md) for the most up to date instructions.* **IMPORTANT!** diff --git a/doc/update/3.1-to-4.0.md b/doc/update/3.1-to-4.0.md index 165f4e6a308..f1ef4df4744 100644 --- a/doc/update/3.1-to-4.0.md +++ b/doc/update/3.1-to-4.0.md @@ -1,4 +1,5 @@ # From 3.1 to 4.0 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/3.1-to-4.0.md) for the most up to date instructions.* ## Important changes diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md index 4149ed6b08d..d89d5235917 100644 --- a/doc/update/4.0-to-4.1.md +++ b/doc/update/4.0-to-4.1.md @@ -1,4 +1,5 @@ # From 4.0 to 4.1 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.0-to-4.1.md) for the most up to date instructions.* ## Important changes diff --git a/doc/update/4.1-to-4.2.md b/doc/update/4.1-to-4.2.md index 5ee8e8781e9..6fe4412ff90 100644 --- a/doc/update/4.1-to-4.2.md +++ b/doc/update/4.1-to-4.2.md @@ -1,4 +1,5 @@ # From 4.1 to 4.2 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.1-to-4.2.md) for the most up to date instructions.* ## 1. Stop server & Resque diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md index cde679598f7..f9faf65f952 100644 --- a/doc/update/4.2-to-5.0.md +++ b/doc/update/4.2-to-5.0.md @@ -1,4 +1,5 @@ # From 4.2 to 5.0 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.2-to-5.0.md) for the most up to date instructions.* ## Warning @@ -41,8 +42,8 @@ git checkout v1.1.0 # copy config cp config.yml.example config.yml -# change url to GitLab instance -# ! make sure url end with '/' like 'https://gitlab.example/' +# change URL to GitLab instance +# ! make sure the URL ends with '/' like 'https://gitlab.example/' vim config.yml # rewrite hooks @@ -111,7 +112,7 @@ sudo chmod -R u+rwX /home/git/gitlab/tmp/pids ``` -## 6. Update init.d script and nginx config +## 6. Update init.d script and Nginx config ```bash # init.d @@ -123,7 +124,7 @@ sudo chmod +x /etc/init.d/gitlab sudo -u git -H cp /home/git/gitlab/config/unicorn.rb /home/git/gitlab/config/unicorn.rb.old sudo -u git -H cp /home/git/gitlab/config/unicorn.rb.example /home/git/gitlab/config/unicorn.rb -#nginx +# Nginx # Replace path from '/home/gitlab/' to '/home/git/' sudo vim /etc/nginx/sites-enabled/gitlab sudo service nginx restart @@ -137,7 +138,7 @@ sudo service gitlab start # check if unicorn and sidekiq started # If not try to logout, also check replaced path from '/home/gitlab/' to '/home/git/' -# in nginx, unicorn, init.d etc +# in Nginx, unicorn, init.d etc ps aux | grep unicorn ps aux | grep sidekiq diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md index 0e597abb1a9..9fbd1f88515 100644 --- a/doc/update/5.0-to-5.1.md +++ b/doc/update/5.0-to-5.1.md @@ -1,4 +1,5 @@ # From 5.0 to 5.1 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.0-to-5.1.md) for the most up to date instructions.* ## Warning diff --git a/doc/update/5.1-to-5.2.md b/doc/update/5.1-to-5.2.md index 6ef559ac9f9..cf9c4e4f770 100644 --- a/doc/update/5.1-to-5.2.md +++ b/doc/update/5.1-to-5.2.md @@ -1,4 +1,5 @@ # From 5.1 to 5.2 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-5.2.md) for the most up to date instructions.* ## Warning diff --git a/doc/update/5.1-to-5.4.md b/doc/update/5.1-to-5.4.md index 8ec56b266ca..97a98ede070 100644 --- a/doc/update/5.1-to-5.4.md +++ b/doc/update/5.1-to-5.4.md @@ -1,4 +1,5 @@ # From 5.1 to 5.4 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-5.4.md) for the most up to date instructions.* Also works starting from 5.2. diff --git a/doc/update/5.1-to-6.0.md b/doc/update/5.1-to-6.0.md index a76b371e6d6..a3fdd92bd2f 100644 --- a/doc/update/5.1-to-6.0.md +++ b/doc/update/5.1-to-6.0.md @@ -1,4 +1,5 @@ # From 5.1 to 6.0 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-6.0.md) for the most up to date instructions.* ## Warning @@ -40,7 +41,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production The migrations in this update are very sensitive to incomplete or inconsistent data. If you have a long-running GitLab installation and some of the previous upgrades did not work out 100% correct this may bite you now. The following can help you have a more smooth upgrade. -### Find projets with invalid project names +### Find projects with invalid project names #### MySQL Login to MySQL: diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md index 61ddf135641..27613aeda07 100644 --- a/doc/update/5.2-to-5.3.md +++ b/doc/update/5.2-to-5.3.md @@ -1,4 +1,5 @@ # From 5.2 to 5.3 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.2-to-5.3.md) for the most up to date instructions.* ## Warning diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md index 8a0d43e3e64..577b9a585ff 100644 --- a/doc/update/5.3-to-5.4.md +++ b/doc/update/5.3-to-5.4.md @@ -1,4 +1,5 @@ # From 5.3 to 5.4 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.3-to-5.4.md) for the most up to date instructions.* ## 0. Backup diff --git a/doc/update/5.4-to-6.0.md b/doc/update/5.4-to-6.0.md index 7bf7bce6aa0..d9c6d9bfb91 100644 --- a/doc/update/5.4-to-6.0.md +++ b/doc/update/5.4-to-6.0.md @@ -1,16 +1,20 @@ # From 5.4 to 6.0 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.4-to-6.0.md) for the most up to date instructions.* ## Warning GitLab 6.0 is affected by critical security vulnerabilities CVE-2013-4490 and CVE-2013-4489. +**You need to follow this guide first, before updating past 6.0, as it contains critical migration steps that are only present +in the `6-0-stable` branch** + ## Deprecations ### Global projects The root (global) namespace for projects is deprecated. -So you need to move all your global projects under groups or users manually before update or they will be automatically moved to the project owner namespace during the update. When a project is moved all its members will receive an email with instructions how to update their git remote url. Please make sure you disable sending email when you do a test of the upgrade. +So you need to move all your global projects under groups or users manually before update or they will be automatically moved to the project owner namespace during the update. When a project is moved all its members will receive an email with instructions how to update their git remote URL. Please make sure you disable sending email when you do a test of the upgrade. ### Teams diff --git a/doc/update/6.0-to-6.1.md b/doc/update/6.0-to-6.1.md index 9d67a3bcb96..c5eba1c01c4 100644 --- a/doc/update/6.0-to-6.1.md +++ b/doc/update/6.0-to-6.1.md @@ -1,4 +1,5 @@ # From 6.0 to 6.1 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.0-to-6.1.md) for the most up to date instructions.* ## Warning diff --git a/doc/update/6.1-to-6.2.md b/doc/update/6.1-to-6.2.md index efa6e43124c..a534528108a 100644 --- a/doc/update/6.1-to-6.2.md +++ b/doc/update/6.1-to-6.2.md @@ -1,4 +1,5 @@ # From 6.1 to 6.2 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.1-to-6.2.md) for the most up to date instructions.* **You should update to 6.1 before installing 6.2 so all the necessary conversions are run.** @@ -35,7 +36,7 @@ sudo -u git -H git checkout v1.7.9 # Addresses multiple critical security vulner ## 4. Install additional packages ```bash -# Add support for lograte for better log file handling +# Add support for logrotate for better log file handling sudo apt-get install logrotate ``` diff --git a/doc/update/6.2-to-6.3.md b/doc/update/6.2-to-6.3.md index e9b3bdd2f54..b08ebde0808 100644 --- a/doc/update/6.2-to-6.3.md +++ b/doc/update/6.2-to-6.3.md @@ -1,4 +1,5 @@ # From 6.2 to 6.3 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.2-to-6.3.md) for the most up to date instructions.* **Requires version: 6.1 or 6.2.** diff --git a/doc/update/6.3-to-6.4.md b/doc/update/6.3-to-6.4.md index 96c2895981d..951d92dfeb5 100644 --- a/doc/update/6.3-to-6.4.md +++ b/doc/update/6.3-to-6.4.md @@ -1,4 +1,5 @@ # From 6.3 to 6.4 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.3-to-6.4.md) for the most up to date instructions.* ## 0. Backup diff --git a/doc/update/6.4-to-6.5.md b/doc/update/6.4-to-6.5.md index 1624296fc3f..0dae9a9fe59 100644 --- a/doc/update/6.4-to-6.5.md +++ b/doc/update/6.4-to-6.5.md @@ -1,4 +1,5 @@ # From 6.4 to 6.5 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.4-to-6.5.md) for the most up to date instructions.* ## 0. Backup diff --git a/doc/update/6.5-to-6.6.md b/doc/update/6.5-to-6.6.md index 544eee17fec..c24e83eb006 100644 --- a/doc/update/6.5-to-6.6.md +++ b/doc/update/6.5-to-6.6.md @@ -1,4 +1,5 @@ # From 6.5 to 6.6 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.5-to-6.6.md) for the most up to date instructions.* ## 0. Backup diff --git a/doc/update/6.6-to-6.7.md b/doc/update/6.6-to-6.7.md index 77ac4d0bfa6..b4298c93429 100644 --- a/doc/update/6.6-to-6.7.md +++ b/doc/update/6.6-to-6.7.md @@ -1,4 +1,5 @@ # From 6.6 to 6.7 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.6-to-6.7.md) for the most up to date instructions.* ## 0. Backup @@ -70,6 +71,9 @@ sudo -u git -H gzip /home/git/gitlab-shell/gitlab-shell.log.1 # Close access to gitlab-satellites for others sudo chmod u+rwx,g=rx,o-rwx /home/git/gitlab-satellites + +# Add directory for uploads +sudo -u git -H mkdir -p /home/git/gitlab/public/uploads ``` ## 5. Start application diff --git a/doc/update/6.7-to-6.8.md b/doc/update/6.7-to-6.8.md index 16f3439c998..4fb90639f16 100644 --- a/doc/update/6.7-to-6.8.md +++ b/doc/update/6.7-to-6.8.md @@ -1,4 +1,5 @@ # From 6.7 to 6.8 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.7-to-6.8.md) for the most up to date instructions.* ## 0. Backup diff --git a/doc/update/6.8-to-6.9.md b/doc/update/6.8-to-6.9.md index 9efb384ff59..b9b8b63f652 100644 --- a/doc/update/6.8-to-6.9.md +++ b/doc/update/6.8-to-6.9.md @@ -1,4 +1,5 @@ # From 6.8 to 6.9 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.8-to-6.9.md) for the most up to date instructions.* ### 0. Backup diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md index 1f3421a799b..236430b5951 100644 --- a/doc/update/6.9-to-7.0.md +++ b/doc/update/6.9-to-7.0.md @@ -1,4 +1,5 @@ # From 6.9 to 7.0 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.9-to-7.0.md) for the most up to date instructions.* ### 0. Backup diff --git a/doc/update/6.x-or-7.x-to-7.5.md b/doc/update/6.x-or-7.x-to-7.12.md index c9b95c62611..5705fb360db 100644 --- a/doc/update/6.x-or-7.x-to-7.5.md +++ b/doc/update/6.x-or-7.x-to-7.12.md @@ -1,6 +1,7 @@ -# From 6.x or 7.x to 7.5 +# From 6.x or 7.x to 7.12 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.12.md) for the most up to date instructions.* -This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.5. +This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.12. ## Global issue numbers @@ -34,7 +35,7 @@ You can check which version you are running with `ruby -v`. If you are you running Ruby 2.0.x, you do not need to upgrade ruby, but can consider doing so for performance reasons. -If you are running Ruby 2.1.1 consider upgrading to 2.1.5, because of the high memory usage of Ruby 2.1.1. +If you are running Ruby 2.1.1 consider upgrading to 2.1.6, because of the high memory usage of Ruby 2.1.1. Install, update dependencies: @@ -46,8 +47,8 @@ Download and compile Ruby: ```bash mkdir /tmp/ruby && cd /tmp/ruby -curl --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.5.tar.gz | tar xz -cd ruby-2.1.5 +curl --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz +cd ruby-2.1.6 ./configure --disable-install-rdoc make sudo make install @@ -70,7 +71,7 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut For GitLab Community Edition: ```bash -sudo -u git -H git checkout 7-5-stable +sudo -u git -H git checkout 7-12-stable ``` OR @@ -78,17 +79,24 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout 7-5-stable-ee +sudo -u git -H git checkout 7-12-stable-ee ``` ## 4. Install additional packages ```bash -# Add support for lograte for better log file handling +# Add support for logrotate for better log file handling sudo apt-get install logrotate # Install pkg-config and cmake, which is needed for the latest versions of rugged sudo apt-get install pkg-config cmake + +# If you want to use Kerberos with GitLab EE for user authentication, install Kerberos header files +# If you don't know what Kerberos is, you can assume you don't need it. +sudo apt-get install libkrb5-dev + +# Install nodejs, javascript runtime required for assets +sudo apt-get install nodejs ``` ## 5. Configure Redis to use sockets @@ -119,7 +127,7 @@ sudo apt-get install pkg-config cmake ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.2.0 +sudo -u git -H git checkout v2.6.3 ``` ## 7. Install libs, migrations, etc. @@ -154,14 +162,12 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab TIP: to see what changed in `gitlab.yml.example` in this release use next command: ``` -git diff 6-0-stable:config/gitlab.yml.example 7-5-stable:config/gitlab.yml.example +git diff 6-0-stable:config/gitlab.yml.example 7-12-stable:config/gitlab.yml.example ``` -* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-5-stable/config/gitlab.yml.example but with your settings. -* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-5-stable/config/unicorn.rb.example but with your settings. -* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.2.0/config.yml.example but with your settings. -* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-5-stable/lib/support/nginx/gitlab but with your settings. -* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-5-stable/lib/support/nginx/gitlab-ssl but with your settings. +* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-12-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-12-stable/config/unicorn.rb.example but with your settings. +* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.0/config.yml.example but with your settings. * Copy rack attack middleware config ```bash @@ -174,6 +180,17 @@ sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab ``` +### Change Nginx settings + +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-12-stable/lib/support/nginx/gitlab but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-12-stable/lib/support/nginx/gitlab-ssl but with your settings. +* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. + +### Check the version of /usr/local/bin/git + +If you installed Git from source into /usr/local/bin/git then please [check +your version](7.11-to-7.12.md). + ## 9. Start application sudo service gitlab start @@ -196,7 +213,7 @@ If all items are green, then congratulations upgrade complete! When using Google omniauth login, changes of the Google account required. Ensure that `Contacts API` and the `Google+ API` are enabled in the [Google Developers Console](https://console.developers.google.com/). -More details can be found at the [integration documentation](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/google.md). +More details can be found at the [integration documentation](../../../master/doc/integration/google.md). ## 12. Optional optimizations for GitLab setups with MySQL databases @@ -217,13 +234,13 @@ mysql -u root -p # Convert all tables to use the InnoDB storage engine (added in GitLab 6.8) SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' ENGINE=InnoDB;') AS 'Copy & run these SQL statements:' FROM information_schema.tables WHERE table_schema = 'gitlabhq_production' AND `ENGINE` <> 'InnoDB' AND `TABLE_TYPE` = 'BASE TABLE'; -# If previous query returned results, copy & run all outputed SQL statements +# If previous query returned results, copy & run all shown SQL statements # Convert all tables to correct character set SET foreign_key_checks = 0; SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;') AS 'Copy & run these SQL statements:' FROM information_schema.tables WHERE table_schema = 'gitlabhq_production' AND `TABLE_COLLATION` <> 'utf8_unicode_ci' AND `TABLE_TYPE` = 'BASE TABLE'; -# If previous query returned results, copy & run all outputed SQL statements +# If previous query returned results, copy & run all shown SQL statements # turn foreign key checks back on SET foreign_key_checks = 1; @@ -231,7 +248,7 @@ SET foreign_key_checks = 1; # Find MySQL users mysql> SELECT user FROM mysql.user WHERE user LIKE '%git%'; -# If git user exists and gitlab user does not exist +# If git user exists and gitlab user does not exist # you are done with the database cleanup tasks mysql> \q @@ -269,11 +286,11 @@ mysql> \q sudo -u git -H editor /home/git/gitlab/config/database.yml ``` -## Things went south? Revert to previous version (6.0) +## Things went south? Revert to previous version (7.0) ### 1. Revert the code to the previous version -Follow the [upgrade guide from 5.4 to 6.0](5.4-to-6.0.md), except for the database migration (the backup is already migrated to the previous version). +Follow the [upgrade guide from 6.9 to 7.0](6.9-to-7.0.md), except for the database migration (the backup is already migrated to the previous version). ### 2. Restore from the backup: diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md index 82bb5708734..a4e9be9946e 100644 --- a/doc/update/7.0-to-7.1.md +++ b/doc/update/7.0-to-7.1.md @@ -1,4 +1,5 @@ # From 7.0 to 7.1 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.0-to-7.1.md) for the most up to date instructions.* ### 0. Backup diff --git a/doc/update/7.1-to-7.2.md b/doc/update/7.1-to-7.2.md index 699111f0143..88cb63d7d41 100644 --- a/doc/update/7.1-to-7.2.md +++ b/doc/update/7.1-to-7.2.md @@ -1,4 +1,5 @@ # From 7.1 to 7.2 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.1-to-7.2.md) for the most up to date instructions.* ## Editable labels diff --git a/doc/update/7.10-to-7.11.md b/doc/update/7.10-to-7.11.md new file mode 100644 index 00000000000..79bc6de1e46 --- /dev/null +++ b/doc/update/7.10-to-7.11.md @@ -0,0 +1,103 @@ +# From 7.10 to 7.11 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. 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 7-11-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-11-stable-ee +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.3 +``` + +### 4. 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 development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# 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 + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 5. Update config 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 to your current `gitlab.yml`. + +``` +git diff origin/7-10-stable:config/gitlab.yml.example origin/7-11-stable:config/gitlab.yml.example +`````` + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. 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 with: + + 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 (7.10) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.9 to 7.10](7.9-to-7.10.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/7.11-to-7.12.md b/doc/update/7.11-to-7.12.md new file mode 100644 index 00000000000..cc14a135926 --- /dev/null +++ b/doc/update/7.11-to-7.12.md @@ -0,0 +1,129 @@ +# From 7.11 to 7.12 + +### 0. Double-check your Git version + +**This notice applies only to /usr/local/bin/git** + +If you compiled Git from source on your GitLab server then please double-check +that you are using a version that protects against CVE-2014-9390. For six +months after this vulnerability became known the GitLab installation guide +still contained instructions that would install the outdated, 'vulnerable' Git +version 2.1.2. + +Run the following command to get your current Git version. + +``` +/usr/local/bin/git --version +``` + +If you see 'No such file or directory' then you did not install Git according +to the outdated instructions from the GitLab installation guide and you can go +to the next step 'Stop server' below. + +If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4, +v2.2.1 or newer. You can use the [instructions in the GitLab source +installation +guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +to install a newer version of Git. + +### 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 7-12-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-12-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.3 +``` + +### 5. 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 development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# 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 + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 6. Update config 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 to your current `gitlab.yml`. + +``` +git diff origin/7-11-stable:config/gitlab.yml.example origin/7-12-stable:config/gitlab.yml.example +`````` + +### 7. Start application + + sudo service gitlab start + sudo service nginx restart + +### 8. 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 with: + + 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 (7.11) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.10 to 7.11](7.10-to-7.11.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/7.2-to-7.3.md b/doc/update/7.2-to-7.3.md index ebdd4ff60fa..18f77d6396e 100644 --- a/doc/update/7.2-to-7.3.md +++ b/doc/update/7.2-to-7.3.md @@ -1,4 +1,5 @@ # From 7.2 to 7.3 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.2-to-7.3.md) for the most up to date instructions.* ### 0. Backup diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md index 2466050ea4c..53e739c06fb 100644 --- a/doc/update/7.3-to-7.4.md +++ b/doc/update/7.3-to-7.4.md @@ -1,4 +1,5 @@ # From 7.3 to 7.4 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.3-to-7.4.md) for the most up to date instructions.* ### 0. Stop server @@ -68,14 +69,17 @@ git diff origin/7-3-stable:config/gitlab.yml.example origin/7-4-stable:config/gi sudo -u git -H editor config/unicorn.rb ``` -#### Change nginx https settings +#### Change Nginx HTTPS settings * HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-4-stable/lib/support/nginx/gitlab-ssl but with your setting #### MySQL Databases: Update database.yml config file -* Add `collation: utf8_general_ci` to config/database.yml as seen in [config/database.yml.mysql](config/database.yml.mysql) +* Add `collation: utf8_general_ci` to `config/database.yml` as seen in [config/database.yml.mysql](/config/database.yml.mysql) +``` +sudo -u git -H editor config/database.yml +``` ### 5. Start application @@ -114,13 +118,13 @@ mysql -u root -p # Convert all tables to use the InnoDB storage engine (added in GitLab 6.8) SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' ENGINE=InnoDB;') AS 'Copy & run these SQL statements:' FROM information_schema.tables WHERE table_schema = 'gitlabhq_production' AND `ENGINE` <> 'InnoDB' AND `TABLE_TYPE` = 'BASE TABLE'; -# If previous query returned results, copy & run all outputed SQL statements +# If previous query returned results, copy & run all shown SQL statements # Convert all tables to correct character set SET foreign_key_checks = 0; SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;') AS 'Copy & run these SQL statements:' FROM information_schema.tables WHERE table_schema = 'gitlabhq_production' AND `TABLE_COLLATION` <> 'utf8_unicode_ci' AND `TABLE_TYPE` = 'BASE TABLE'; -# If previous query returned results, copy & run all outputed SQL statements +# If previous query returned results, copy & run all shown SQL statements # turn foreign key checks back on SET foreign_key_checks = 1; diff --git a/doc/update/7.4-to-7.5.md b/doc/update/7.4-to-7.5.md index c12becc1e14..673eab3c56e 100644 --- a/doc/update/7.4-to-7.5.md +++ b/doc/update/7.4-to-7.5.md @@ -71,21 +71,10 @@ There are new configuration options available for gitlab.yml. View them with the git diff origin/7-4-stable:config/gitlab.yml.example origin/7-5-stable:config/gitlab.yml.example ``` -#### Change timeout for unicorn - -``` -# set timeout to 60 -sudo -u git -H editor config/unicorn.rb -``` - -#### Change nginx https settings - -* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-5-stable/lib/support/nginx/gitlab-ssl but with your setting - -#### MySQL Databases: Update database.yml config file - -* Add `collation: utf8_general_ci` to config/database.yml as seen in [config/database.yml.mysql](config/database.yml.mysql) +#### Change Nginx settings +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your setting ### 6. Start application @@ -104,82 +93,6 @@ To make sure you didn't miss anything run a more thorough check with: If all items are green, then congratulations upgrade is complete! - -### 8. Optional optimizations for GitLab setups with MySQL databases - -Only applies if running MySQL database created with GitLab 6.7 or earlier. If you are not experiencing any issues you may not need the following instructions however following them will bring your database in line with the latest recommended installation configuration and help avoid future issues. Be sure to follow these directions exactly. These directions should be safe for any MySQL instance but to be sure make a current MySQL database backup beforehand. - -``` -# Stop GitLab -sudo service gitlab stop - -# Secure your MySQL installation (added in GitLab 6.2) -sudo mysql_secure_installation - -# Login to MySQL -mysql -u root -p - -# do not type the 'mysql>', this is part of the prompt - -# Convert all tables to use the InnoDB storage engine (added in GitLab 6.8) -SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' ENGINE=InnoDB;') AS 'Copy & run these SQL statements:' FROM information_schema.tables WHERE table_schema = 'gitlabhq_production' AND `ENGINE` <> 'InnoDB' AND `TABLE_TYPE` = 'BASE TABLE'; - -# If previous query returned results, copy & run all outputed SQL statements - -# Convert all tables to correct character set -SET foreign_key_checks = 0; -SELECT CONCAT('ALTER TABLE gitlabhq_production.', table_name, ' CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;') AS 'Copy & run these SQL statements:' FROM information_schema.tables WHERE table_schema = 'gitlabhq_production' AND `TABLE_COLLATION` <> 'utf8_unicode_ci' AND `TABLE_TYPE` = 'BASE TABLE'; - -# If previous query returned results, copy & run all outputed SQL statements - -# turn foreign key checks back on -SET foreign_key_checks = 1; - -# Find MySQL users -mysql> SELECT user FROM mysql.user WHERE user LIKE '%git%'; - -# If git user exists and gitlab user does not exist -# you are done with the database cleanup tasks -mysql> \q - -# If both users exist skip to Delete gitlab user - -# Create new user for GitLab (changed in GitLab 6.4) -# change $password in the command below to a real password you pick -mysql> CREATE USER 'git'@'localhost' IDENTIFIED BY '$password'; - -# Grant the git user necessary permissions on the database -mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES ON `gitlabhq_production`.* TO 'git'@'localhost'; - -# Delete the old gitlab user -mysql> DELETE FROM mysql.user WHERE user='gitlab'; - -# Quit the database session -mysql> \q - -# Try connecting to the new database with the new user -sudo -u git -H mysql -u git -p -D gitlabhq_production - -# Type the password you replaced $password with earlier - -# You should now see a 'mysql>' prompt - -# Quit the database session -mysql> \q - -# Update database configuration details -# See config/database.yml.mysql for latest recommended configuration details -# Remove the reaping_frequency setting line if it exists (removed in GitLab 6.8) -# Set production -> pool: 10 (updated in GitLab 5.3) -# Set production -> username: git -# Set production -> password: the password your replaced $password with earlier -sudo -u git -H editor /home/git/gitlab/config/database.yml - -# Run thorough check -sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production -``` - - ## Things went south? Revert to previous version (7.4) ### 1. Revert the code to the previous version diff --git a/doc/update/7.5-to-7.6.md b/doc/update/7.5-to-7.6.md index deee73fe560..35cd437fdc4 100644 --- a/doc/update/7.5-to-7.6.md +++ b/doc/update/7.5-to-7.6.md @@ -1,7 +1,5 @@ # From 7.5 to 7.6 -**7.6 is not yet released. This is a preliminary upgrade guide.** - ### 0. Stop server sudo service gitlab stop @@ -39,12 +37,14 @@ sudo -u git -H git checkout 7-6-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.2.0 +sudo -u git -H git checkout v2.4.0 ``` ### 4. Install libs, migrations, etc. ```bash +sudo apt-get install libkrb5-dev + cd /home/git/gitlab # MySQL installations (note: the line below states '--without ... postgres') diff --git a/doc/update/7.6-to-7.7.md b/doc/update/7.6-to-7.7.md new file mode 100644 index 00000000000..910c7dcdd3c --- /dev/null +++ b/doc/update/7.6-to-7.7.md @@ -0,0 +1,119 @@ +# From 7.6 to 7.7 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. 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 7-7-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-7-stable-ee +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.4.2 +``` + +### 4. Install libs, migrations, etc. + +```bash +sudo apt-get install libkrb5-dev + +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# 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 + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 5. Update config 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 to your current `gitlab.yml`. + +``` +git diff origin/7-6-stable:config/gitlab.yml.example origin/7-7-stable:config/gitlab.yml.example +``` + +#### Change Nginx settings + +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your setting + +#### Setup time zone (optional) + +Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it. + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. 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 with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations upgrade is complete! + +### 8. GitHub settings (if applicable) + +If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it +only contains a root URL (ex. `https://gitlab.example.com/`) + +## Things went south? Revert to previous version (7.6) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.5 to 7.6](7.5-to-7.6.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/7.7-to-7.8.md b/doc/update/7.7-to-7.8.md new file mode 100644 index 00000000000..46ca163c1bb --- /dev/null +++ b/doc/update/7.7-to-7.8.md @@ -0,0 +1,120 @@ +# From 7.7 to 7.8 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. 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 7-8-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-8-stable-ee +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.5.4 +``` + +### 4. Install libs, migrations, etc. + +```bash +sudo apt-get install libkrb5-dev + +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# 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 + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 5. Update config 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 to your current `gitlab.yml`. + +``` +git diff origin/7-7-stable:config/gitlab.yml.example origin/7-8-stable:config/gitlab.yml.example +``` + +#### Change Nginx settings + +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your settings. +* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. + +#### Setup time zone (optional) + +Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it. + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. 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 with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations upgrade is complete! + +### 8. GitHub settings (if applicable) + +If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it +only contains a root URL (ex. `https://gitlab.example.com/`) + +## Things went south? Revert to previous version (7.7) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.6 to 7.7](7.6-to-7.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/7.8-to-7.9.md b/doc/update/7.8-to-7.9.md new file mode 100644 index 00000000000..6ffa21c6141 --- /dev/null +++ b/doc/update/7.8-to-7.9.md @@ -0,0 +1,122 @@ +# From 7.8 to 7.9 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. 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 7-9-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-9-stable-ee +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.0 +``` + +### 4. Install libs, migrations, etc. + +Please refer to the [Node.js setup documentation](https://github.com/joyent/node/wiki/installing-node.js-via-package-manager#debian-and-ubuntu-based-linux-distributions) if you aren't running default GitLab server setup. + +```bash +sudo apt-get install nodejs + +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# 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 + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 5. Update config 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 to your current `gitlab.yml`. + +``` +git diff origin/7-8-stable:config/gitlab.yml.example origin/7-9-stable:config/gitlab.yml.example +``` + +#### Change Nginx settings + +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your settings. +* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. + +#### Setup time zone (optional) + +Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it. + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. 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 with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations upgrade is complete! + +### 8. GitHub settings (if applicable) + +If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it +only contains a root URL (ex. `https://gitlab.example.com/`) + +## Things went south? Revert to previous version (7.8) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.7 to 7.8](7.7-to-7.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/7.9-to-7.10.md b/doc/update/7.9-to-7.10.md new file mode 100644 index 00000000000..d1179dc2ec7 --- /dev/null +++ b/doc/update/7.9-to-7.10.md @@ -0,0 +1,118 @@ +# From 7.9 to 7.10 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. 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 7-10-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-10-stable-ee +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.2 +``` + +### 4. 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 development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# 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 + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 5. Update config 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 to your current `gitlab.yml`. + +``` +git diff origin/7-9-stable:config/gitlab.yml.example origin/7-10-stable:config/gitlab.yml.example +``` + +#### Change Nginx settings + +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your settings. +* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. + +#### Setup time zone (optional) + +Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it. + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. 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 with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations upgrade is complete! + +### 8. GitHub settings (if applicable) + +If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it +only contains a root URL (ex. `https://gitlab.example.com/`) + +## Things went south? Revert to previous version (7.9) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.8 to 7.9](7.8-to-7.9.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 30e9137d7b7..0472537eeb5 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -4,13 +4,13 @@ Depending on the installation method and your GitLab version, there are multiple - [Omnibus update guide](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md) contains the steps needed to update a GitLab [package](https://about.gitlab.com/downloads/). -## Manual Installation +## Installation from source -- [The individual upgrade guides](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update) are for those who have installed GitLab manually. +- [The individual upgrade guides](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update) are for those who have installed GitLab from source. - [The CE to EE update guides](https://gitlab.com/subscribers/gitlab-ee/tree/master/doc/update) are for subscribers of the Enterprise Edition only. The steps are very similar to a version upgrade: stop the server, get the code, update config files for the new functionality, install libs and do migrations, update the init script, start the application and check the application status. -- [Upgrader](upgrader.md) is an automatic ruby script that performs the update for manual installations. +- [Upgrader](upgrader.md) is an automatic ruby script that performs the update for installations from source. - [Patch versions](patch_versions.md) guide includes the steps needed for a patch version, eg. 6.2.0 to 6.2.1. ## Miscellaneous -- [MySQL to PostgreSQL](mysql_to_postgresql.md) guides you through migrating your database from MySQL to PostrgreSQL. +- [MySQL to PostgreSQL](mysql_to_postgresql.md) guides you through migrating your database from MySQL to PostgreSQL. diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md index 695c083d361..2c43cf59c1f 100644 --- a/doc/update/mysql_to_postgresql.md +++ b/doc/update/mysql_to_postgresql.md @@ -1,6 +1,7 @@ # Migrating GitLab from MySQL to Postgres +*Make sure you view this [guide from the `master` branch](../../../master/doc/update/mysql_to_postgresql.md) for the most up to date instructions.* -If you are replacing MySQL with Postgres while keeping GitLab on the same server all you need to do is to export from MySQL, import into Postgres and rebuild the indexes as described below. If you are also moving GitLab to another server, or if you are switching to omnibus-gitlab, you may want to use a GitLab backup file. The second part of this documents explains the procedure to do this. +If you are replacing MySQL with Postgres while keeping GitLab on the same server all you need to do is to export from MySQL, convert the resulting SQL file, and import it into Postgres. If you are also moving GitLab to another server, or if you are switching to omnibus-gitlab, you may want to use a GitLab backup file. The second part of this documents explains the procedure to do this. ## Export from MySQL and import into Postgres @@ -11,15 +12,13 @@ sudo service gitlab stop # Update /home/git/gitlab/config/database.yml -git clone https://github.com/gitlabhq/mysql-postgresql-converter.git +git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab cd mysql-postgresql-converter -mysqldump --compatible=postgresql --default-character-set=utf8 -r databasename.mysql -u root gitlabhq_production -python db_converter.py databasename.mysql databasename.psql +mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production -p +python db_converter.py gitlabhq_production.mysql gitlabhq_production.psql # Import the database dump as the application database user -sudo -u git psql -f databasename.psql -d gitlabhq_production - -# Rebuild indexes (see below) +sudo -u git psql -f gitlabhq_production.psql -d gitlabhq_production # Install gems for PostgreSQL (note: the line below states '--without ... mysql') sudo -u git -H bundle install --without development test mysql --deployment @@ -27,55 +26,10 @@ sudo -u git -H bundle install --without development test mysql --deployment sudo service gitlab start ``` -## Rebuild indexes - -The lanyrd database converter script does not preserve all indexes, so we have to recreate them ourselves after migrating from MySQL. It is not necessary to shut down GitLab for this process. - -### For non-omnibus installations - -On non-omnibus installations (distributed using Git) we retrieve the index declarations from version control using `git stash`. - -``` -# Clone the database converter on your Postgres-backed GitLab server -cd /tmp -git clone https://github.com/gitlabhq/mysql-postgresql-converter.git - -cd /home/git/gitlab - -# Stash changes to db/schema.rb to make sure we can find the right index statements -sudo -u git -H git stash - -# Generate add_index.rb -ruby /tmp/mysql-postgresql-converter/add_index_statements.rb db/schema.rb > /tmp/mysql-postgresql-converter/add_index.rb - -# Create the indexes -sudo -u git -H bundle exec rails runner -e production 'eval $stdin.read' < /tmp/mysql-postgresql-converter/add_index.rb -``` - -### For omnibus-gitlab installations - -On omnibus-gitlab we need to get the index declarations from a file called `schema.rb.bundled`. For versions older than 6.9, we need to download the file. - -``` -# Clone the database converter on your Postgres-backed GitLab server -cd /tmp -/opt/gitlab/embedded/bin/git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -cd /tmp/mysql-postgresql-converter - -# Download schema.rb.bundled if necessary -test -e /opt/gitlab/embedded/service/gitlab-rails/db/schema.rb.bundled || sudo /opt/gitlab/embedded/bin/curl -o /opt/gitlab/embedded/service/gitlab-rails/db/schema.rb.bundled https://gitlab.com/gitlab-org/gitlab-ce/raw/v6.9.1/db/schema.rb - -# Generate add_index.rb -/opt/gitlab/embedded/bin/ruby add_index_statements.rb /opt/gitlab/embedded/service/gitlab-rails/db/schema.rb.bundled > add_index.rb - -# Create the indexes -/opt/gitlab/bin/gitlab-rails runner 'eval $stdin.read' < add_index.rb -``` - ## Converting a GitLab backup file from MySQL to Postgres **Note:** Please make sure to have Python 2.7.x (or higher) installed. -GitLab backup files (<timestamp>_gitlab_backup.tar) contain a SQL dump. Using the lanyrd database converter we can replace a MySQL database dump inside the tar file with a Postgres database dump. This can be useful if you are moving to another server. +GitLab backup files (`<timestamp>_gitlab_backup.tar`) contain a SQL dump. Using the lanyrd database converter we can replace a MySQL database dump inside the tar file with a Postgres database dump. This can be useful if you are moving to another server. ``` # Stop GitLab @@ -94,10 +48,10 @@ sudo -u git -H mv tmp/backups/TIMESTAMP_gitlab_backup.tar tmp/backups/postgresql # Create a separate database dump with PostgreSQL compatibility cd tmp/backups/postgresql -sudo -u git -H mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production +sudo -u git -H mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production -p # Clone the database converter -sudo -u git -H git clone https://github.com/gitlabhq/mysql-postgresql-converter.git +sudo -u git -H git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab # Convert gitlabhq_production.mysql sudo -u git -H mkdir db diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index 629c46ad030..e29ee2a7b3d 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -1,4 +1,5 @@ # Universal update guide for patch versions +*Make sure you view this [upgrade guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/patch_versions.md) from the `master` branch for the most up to date instructions.* For example from 6.2.0 to 6.2.1, also see the [semantic versioning specification](http://semver.org/). diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index 0a9f242d9ab..6854250dab7 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -1,5 +1,11 @@ # GitLab Upgrader +*DEPRECATED* We recommend to [switch to the Omnibus package and repository server](https://about.gitlab.com/update/) instead of using this script. + +Although deprecated, if someone wants to make this script into a gem or otherwise improve it merge requests are welcome. + +*Make sure you view this [upgrade guide from the 'master' branch](../../../master/doc/update/upgrader.md) for the most up to date instructions.* + GitLab Upgrader - a ruby script that allows you easily upgrade GitLab to latest minor version. For example it can update your application from 6.4 to latest GitLab 6 version (like 6.6.1). @@ -23,14 +29,15 @@ If you have local changes to your GitLab repository the script will stash them a ## 2. Run GitLab upgrade tool -Note: GitLab 7.2 adds `pkg-config` and `cmake` as dependency. Please check the dependencies in the [installation guide.](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +Please replace X.X.X with the [latest GitLab release](https://packages.gitlab.com/gitlab/gitlab-ce). + +GitLab 7.9 adds `nodejs` as a dependency. GitLab 7.6 adds `libkrb5-dev` as a dependency (installed by default on Ubuntu and OSX). GitLab 7.2 adds `pkg-config` and `cmake` as dependency. Please check the dependencies in the [installation guide.](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) - # Starting with GitLab version 7.0 upgrader script has been moved to bin directory cd /home/git/gitlab - if [ -f bin/upgrade.rb ]; then sudo -u git -H ruby bin/upgrade.rb; else sudo -u git -H ruby script/upgrade.rb; fi + sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"vX.X.X"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' # to perform a non-interactive install (no user input required) you can add -y - # if [ -f bin/upgrade.rb ]; then sudo -u git -H ruby bin/upgrade.rb -y; else sudo -u git -H ruby script/upgrade.rb -y; fi + # sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"vX.X.X"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' -- -y ## 3. Start application @@ -59,17 +66,20 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` You've read through the entire guide and probably already did all the steps one by one. -Here is a one line command with step 1 to 5 for the next time you upgrade: +Below is a one line command with step 1 to 5 for the next time you upgrade. + +Please replace X.X.X with the [latest GitLab release](https://packages.gitlab.com/gitlab/gitlab-ce). ```bash cd /home/git/gitlab; \ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production; \ sudo service gitlab stop; \ - if [ -f bin/upgrade.rb ]; then sudo -u git -H ruby bin/upgrade.rb -y; else sudo -u git -H ruby script/upgrade.rb -y; fi; \ + sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"vX.X.X"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' -- -y; \ cd /home/git/gitlab-shell; \ sudo -u git -H git fetch; \ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`; \ cd /home/git/gitlab; \ sudo service gitlab start; \ - sudo service nginx restart; sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production -``` + sudo service nginx restart; \ + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +```
\ No newline at end of file diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index f19517c0f18..73717ffc7d6 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -12,28 +12,39 @@ If you send a web hook to an SSL endpoint [the certificate will not be verified] Triggered when you push to the repository except when pushing tags. +**Request header**: + +``` +X-Gitlab-Event: Push Hook +``` + **Request body:** ```json { + "object_kind": "push", "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "ref": "refs/heads/master", "user_id": 4, "user_name": "John Smith", + "user_email": "john@example.com", "project_id": 15, "repository": { "name": "Diaspora", - "url": "git@example.com:diaspora.git", + "url": "git@example.com:mike/diasporadiaspora.git", "description": "", - "homepage": "http://example.com/diaspora" + "homepage": "http://example.com/mike/diaspora", + "git_http_url":"http://example.com/mike/diaspora.git", + "git_ssh_url":"git@example.com:mike/diaspora.git", + "visibility_level":0 }, "commits": [ { "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", "message": "Update Catalan translation to e38cb41.", "timestamp": "2011-12-12T14:27:31+02:00", - "url": "http://example.com/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", "author": { "name": "Jordi Mallach", "email": "jordi@softcatala.org" @@ -43,7 +54,7 @@ Triggered when you push to the repository except when pushing tags. "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "message": "fixed readme", "timestamp": "2012-01-03T23:36:29+02:00", - "url": "http://example.com/diaspora/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "author": { "name": "GitLab dev user", "email": "gitlabdev@dv6700.(none)" @@ -54,10 +65,52 @@ Triggered when you push to the repository except when pushing tags. } ``` +## Tag events + +Triggered when you create (or delete) tags to the repository. + +**Request header**: + +``` +X-Gitlab-Event: Tag Push Hook +``` + + +**Request body:** + +```json +{ + "object_kind": "tag_push", + "ref": "refs/tags/v1.0.0", + "before": "0000000000000000000000000000000000000000", + "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", + "user_id": 1, + "user_name": "John Smith", + "project_id": 1, + "repository": { + "name": "jsmith", + "url": "ssh://git@example.com/jsmith/example.git", + "description": "", + "homepage": "http://example.com/jsmith/example", + "git_http_url":"http://example.com/jsmith/example.git", + "git_ssh_url":"git@example.com:jsmith/example.git", + "visibility_level":0 + }, + "commits": [], + "total_commits_count": 0 +} +``` + ## Issues events Triggered when a new issue is created or an existing issue was updated/closed/reopened. +**Request header**: + +``` +X-Gitlab-Event: Issue Hook +``` + **Request body:** ```json @@ -87,10 +140,295 @@ Triggered when a new issue is created or an existing issue was updated/closed/re } } ``` +## Comment events + +Triggered when a new comment is made on commits, merge requests, issues, and code snippets. +The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The +payload will also include information about the target of the comment. For example, +a comment on a issue will include the specific issue information under the `issue` key. +Valid target types: + +1. `commit` +2. `merge_request` +3. `issue` +4. `snippet` + +### Comment on commit + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Adminstrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "repository": { + "name": "Gitlab Test", + "url": "http://localhost/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1243, + "note": "This is a commit comment. How does this work?", + "noteable_type": "Commit", + "author_id": 1, + "created_at": "2015-05-17 18:08:09 UTC", + "updated_at": "2015-05-17 18:08:09 UTC", + "project_id": 5, + "attachment":null, + "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1", + "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "noteable_id": null, + "system": false, + "st_diff": { + "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n", + "new_path": "six", + "old_path": "six", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false + }, + "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243" + }, + "commit": { + "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "timestamp": "2014-02-27T10:06:20+02:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "author": { + "name": "Dmitriy Zaporozhets", + "email": "dmitriy.zaporozhets@gmail.com" + } + } +} +``` + +### Comment on merge request + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1244, + "note": "This MR needs work.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2015-05-17 18:21:36 UTC", + "updated_at": "2015-05-17 18:21:36 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 7, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244" + }, + "merge_request": { + "id": 7, + "target_branch": "markdown", + "source_branch": "master", + "source_project_id": 5, + "author_id": 8, + "assignee_id": 28, + "title": "Tempora et eos debitis quae laborum et.", + "created_at": "2015-03-01 20:12:53 UTC", + "updated_at": "2015-03-21 18:27:27 UTC", + "milestone_id": 11, + "state": "opened", + "merge_status": "cannot_be_merged", + "target_project_id": 5, + "iid": 1, + "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.", + "position": 0, + "locked_at": null, + "source": { + "name": "Gitlab Test", + "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", + "http_url": "http://example.com/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility_level": 10 + }, + "target": { + "name": "Gitlab Test", + "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", + "http_url": "http://example.com/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility_level": 10 + }, + "last_commit": { + "id": "562e173be03b8ff2efb05345d12df18815438a4b", + "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n", + "timestamp": "2015-04-08T21: 00:25-07:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b", + "author": { + "name": "John Smith", + "email": "john@example.com" + } + } + } +} +``` + +### Comment on issue + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Adminstrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1241, + "note": "Hello world", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2015-05-17 17:06:40 UTC", + "updated_at": "2015-05-17 17:06:40 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 92, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241" + }, + "issue": { + "id": 92, + "title": "test", + "assignee_id": null, + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-12 14:53:17 UTC", + "updated_at": "2015-04-26 08:28:42 UTC", + "position": 0, + "branch_name": null, + "description": "test", + "milestone_id": null, + "state": "closed", + "iid": 17 + } +} +``` + +### Comment on code snippet + + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +``` +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1245, + "note": "Is this snippet doing what it's supposed to be doing?", + "noteable_type": "Snippet", + "author_id": 1, + "created_at": "2015-05-17 18:35:50 UTC", + "updated_at": "2015-05-17 18:35:50 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 53, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245" + }, + "snippet": { + "id": 53, + "title": "test", + "content": "puts 'Hello world'", + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-09 02:40:38 UTC", + "updated_at": "2015-04-09 02:40:38 UTC", + "file_name": "test.rb", + "expires_at": null, + "type": "ProjectSnippet", + "visibility_level": 0 + } +} +``` ## Merge request events -Triggered when a new merge request is created or an existing merge request was updated/merged/closed. +Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch. + +**Request header**: + +``` +X-Gitlab-Event: Merge Request Hook +``` **Request body:** @@ -143,7 +481,9 @@ Triggered when a new merge request is created or an existing merge request was u "name": "GitLab dev user", "email": "gitlabdev@dv6700.(none)" } - } + }, + "url": "http://example.com/diaspora/merge_requests/1", + "action": "open" } } ``` diff --git a/doc/workflow/README.md b/doc/workflow/README.md index c26d85e9955..f1959d30139 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -1,8 +1,15 @@ -- [Workflow](workflow.md) -- [Project Features](project_features.md) +# Workflow + - [Authorization for merge requests](authorization_for_merge_requests.md) +- [Change your time zone](timezone.md) +- [Feature branch workflow](workflow.md) +- [GitLab Flow](gitlab_flow.md) - [Groups](groups.md) +- [Keyboard shortcuts](shortcuts.md) - [Labels](labels.md) -- [GitLab Flow](gitlab_flow.md) - [Notifications](notifications.md) -- [Migrating from SVN to GitLab](migrating_from_svn.md) +- [Project Features](project_features.md) +- [Project forking workflow](forking_workflow.md) +- [Protected branches](protected_branches.md) +- [Web Editor](web_editor.md) +- ["Work In Progress" Merge Requests](wip_merge_requests.md) diff --git a/doc/workflow/forking/branch_select.png b/doc/workflow/forking/branch_select.png Binary files differnew file mode 100644 index 00000000000..275f64d113b --- /dev/null +++ b/doc/workflow/forking/branch_select.png diff --git a/doc/workflow/forking/fork_button.png b/doc/workflow/forking/fork_button.png Binary files differnew file mode 100644 index 00000000000..def4266476a --- /dev/null +++ b/doc/workflow/forking/fork_button.png diff --git a/doc/workflow/forking/groups.png b/doc/workflow/forking/groups.png Binary files differnew file mode 100644 index 00000000000..3ac64b3c8e7 --- /dev/null +++ b/doc/workflow/forking/groups.png diff --git a/doc/workflow/forking/merge_request.png b/doc/workflow/forking/merge_request.png Binary files differnew file mode 100644 index 00000000000..2dc00ed08a1 --- /dev/null +++ b/doc/workflow/forking/merge_request.png diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md new file mode 100644 index 00000000000..8edf7c6ab3d --- /dev/null +++ b/doc/workflow/forking_workflow.md @@ -0,0 +1,36 @@ +# Project forking workflow + +Forking a project to your own namespace is useful if you have no write access to the project you want to contribute +to. If you do have write access or can request it we recommend working together in the same repository since it is simpler. +See our **[GitLab Flow](https://about.gitlab.com/2014/09/29/gitlab-flow/)** article for more information about using +branches to work together. + +## Creating a fork + +In order to create a fork of a project, all you need to do is click on the fork button located on the top right side +of the screen, close to the project's URL and right next to the stars button. + + + +Once you do that you'll be presented with a screen where you can choose the namespace to fork to. Only namespaces +(groups and your own namespace) where you have write access to, will be shown. Click on the namespace to create your +fork there. + + + +After the forking is done, you can start working on the newly created repository. There you will have full +[Owner](../permissions/permissions.md) access, so you can set it up as you please. + +## Merging upstream + +Once you are ready to send your code back to the main project, you need to create a merge request. Choose your forked +project's main branch as the source and the original project's main branch as the destination and create the merge request. + + + +You can then assign the merge request to someone to have them review your changes. Upon pressing the 'Accept Merge Request' +button, your changes will be added to the repository and branch you're merging into. + + + + diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index f8fd7c97e2a..0e87dc74217 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -1,6 +1,6 @@  -# Introduction +## Introduction Version management with git makes branching and merging much easier than older versioning systems such as SVN. This allows a wide variety of branching strategies and workflows. @@ -29,9 +29,9 @@ People have a hard time figuring out which branch they should develop on or depl Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html) We think there is still room for improvement and will detail a set of practices we call GitLab flow. -# Git flow and its problems +## Git flow and its problems -[ +[ Git flow was one of the first proposals to use git branches and it has gotten a lot of attention. It advocates a master branch and a separate develop branch as well as supporting branches for features, releases and hotfixes. @@ -43,14 +43,14 @@ Since most tools automatically make the master branch the default one and displa The second problem of git flow is the complexity introduced by the hotfix and release branches. These branches can be a good idea for some organizations but are overkill for the vast majority of them. Nowadays most organizations practice continuous delivery which means that your default branch can be deployed. -This means that hotfixed and release branches can be prevented including all the ceremony they introduce. +This means that hotfix and release branches can be prevented including all the ceremony they introduce. An example of this ceremony is the merging back of release branches. Though specialized tools do exist to solve this, they require documentation and add complexity. Frequently developers make a mistake and for example changes are only merged into master and not into the develop branch. The root cause of these errors is that git flow is too complex for most of the use cases. And doing releases doesn't automatically mean also doing hotfixes. -# GitHub flow as a simpler alternative +## GitHub flow as a simpler alternative  @@ -62,13 +62,13 @@ Merging everything into the master branch and deploying often means you minimize But this flow still leaves a lot of questions unanswered regarding deployments, environments, releases and integrations with issues. With GitLab flow we offer additional guidance for these questions. -# Production branch with GitLab flow +## Production branch with GitLab flow  GitHub flow does assume you are able to deploy to production every time you merge a feature branch. This is possible for SaaS applications but are many cases where this is not possible. -One would be a situation where you are not in control of the exact release moment, for example an iOS application that needs to pass AppStore validation. +One would be a situation where you are not in control of the exact release moment, for example an iOS application that needs to pass App Store validation. Another example is when you have deployment windows (workdays from 10am to 4pm when the operations team is at full capacity) but you also merge code at other times. In these cases you can make a production branch that reflects the deployed code. You can deploy a new version by merging in master to the production branch. @@ -78,7 +78,7 @@ This time is pretty accurate if you automatically deploy your production branch. If you need a more exact time you can have your deployment script create a tag on each deployment. This flow prevents the overhead of releasing, tagging and merging that is common to git flow. -# Environment branches with GitLab flow +## Environment branches with GitLab flow  @@ -93,14 +93,14 @@ If master is good to go (it should be if you a practicing [continuous delivery]( If this is not possible because more manual testing is required you can send merge requests from the feature branch to the downstream branches. An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](http://teatro.io/). -# Release branches with GitLab flow +## Release branches with GitLab flow - + Only in case you need to release software to the outside world you need to work with release branches. In this case, each branch contains a minor version (2-3-stable, 2-4-stable, etc.). The stable branch uses master as a starting point and is created as late as possible. -By branching as late as possible you minimize the time you have to apply bugfixes to multiple branches. +By branching as late as possible you minimize the time you have to apply bug fixes to multiple branches. After a release branch is announced, only serious bug fixes are included in the release branch. If possible these bug fixes are first merged into master and then cherry-picked into the release branch. This way you can't forget to cherry-pick them into master and encounter the same bug on subsequent releases. @@ -109,7 +109,7 @@ Every time a bug-fix is included in a release branch the patch version is raised Some projects also have a stable branch that points to the same commit as the latest released branch. In this flow it is not common to have a production branch (or git flow master branch). -# Merge/pull requests with GitLab flow +## Merge/pull requests with GitLab flow  @@ -134,7 +134,7 @@ If the assigned person does not feel comfortable they can close the merge reques 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](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/permissions/permissions.md). So if you want to merge it into a protected branch you assign it to someone with master authorizations. -# Issues with GitLab flow +## Issues with GitLab flow  @@ -168,7 +168,7 @@ In this case it is no problem to reuse the same branch name since it was deleted At any time there is at most one branch for every issue. It is possible that one feature branch solves more than one issue. -# Linking and closing issues from merge requests +## Linking and closing issues from merge requests  @@ -177,11 +177,11 @@ In GitLab this creates a comment in the issue that the merge requests mentions t And the merge request shows the linked issues. These issues are closed once code is merged into the default branch. -If you only want to make the reference without closing the issue you can also just mention it: "Ducktyping is preferred. #12". +If you only want to make the reference without closing the issue you can also just mention it: "Duck typing is preferred. #12". If you have an issue that spans across multiple repositories, the best thing is to create an issue for each repository and link all issues to a parent issue. -# Squashing commits with rebase +## Squashing commits with rebase  @@ -189,7 +189,7 @@ With git you can use an interactive rebase (rebase -i) to squash multiple commit 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. -When you rebase you change the identifier (SHA1) of the commit and this is confusing. +When you rebase you change the identifier (SHA-1) of the commit and this is confusing. If you do that the same change will be known under multiple identifiers and this can cause much confusion. If people already reviewed your code it will be hard for them to review only the improvements you made since then if you have rebased everything into one commit. @@ -207,7 +207,7 @@ If you revert a merge and you change your mind, revert the revert instead of mer Being able to revert a merge is a good reason always to create a merge commit when you merge manually with the `--no-ff` option. Git management software will always create a merge commit when you accept a merge request. -# Do not order commits with rebase +## Do not order commits with rebase  @@ -231,8 +231,8 @@ The last reason for creating merge commits is having long lived branches that yo Martin Fowler, in [his article about feature branches](http://martinfowler.com/bliki/FeatureBranch.html) talks about this Continuous Integration (CI). At GitLab we are guilty of confusing CI with branch testing. Quoting Martin Fowler: "I've heard people say they are doing CI because they are running builds, perhaps using a CI server, on every branch with every commit. That's continuous building, and a Good Thing, but there's no integration, so it's not CI.". -The solution to prevent many merge commits is to keep your feature branches shortlived, the vast majority should take less than one day of work. -If your feature branches commenly take more than a day of work, look into ways to create smaller units of work and/or use [feature toggles](http://martinfowler.com/bliki/FeatureToggle.html). +The solution to prevent many merge commits is to keep your feature branches short-lived, the vast majority should take less than one day of work. +If your feature branches commonly take more than a day of work, look into ways to create smaller units of work and/or use [feature toggles](http://martinfowler.com/bliki/FeatureToggle.html). As for the long running branches that take more than one day there are two strategies. In a CI strategy you can merge in master at the start of the day to prevent painful merges at a later time. In a synchronization point strategy you only merge in from well defined points in time, for example a tagged release. @@ -244,7 +244,7 @@ Developing software happen in small messy steps and it is OK to have your histor You can use tools to view the network graphs of commits and understand the messy history that created your code. If you rebase code the history is incorrect, and there is no way for tools to remedy this because they can't deal with changing commit identifiers. -# Voting on merge requests +## Voting on merge requests  @@ -252,7 +252,7 @@ It is common to voice approval or disapproval by using +1 or -1 emoticons. In GitLab the +1 and -1 are aggregated and shown at the top of the merge request. As a rule of thumb anything that doesn't have two times more +1's than -1's is suspect and should not be merged yet. -# Pushing and removing branches +## Pushing and removing branches  @@ -266,7 +266,7 @@ This ensures that the branch overview in the repository management software show This also ensures that when someone reopens the issue a new branch with the same name can be used without problem. When you reopen an issue you need to create a new merge request. -# Committing often and with the right message +## Committing often and with the right message  @@ -282,7 +282,7 @@ Some words that are bad commit messages because they don't contain munch informa The word fix or fixes is also a red flag, unless it comes after the commit sentence and references an issue number. To see more information about the formatting of commit messages please see this great [blog post by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). -# Testing before merging +## Testing before merging  @@ -299,7 +299,7 @@ If there are no merge conflicts and the feature branches are short lived the ris If there are merge conflicts you merge the master branch into the feature branch and the CI server will rerun the tests. If you have long lived feature branches that last for more than a few days you should make your issues smaller. -# Merging in other code +## Merging in other code  diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md new file mode 100644 index 00000000000..19395657719 --- /dev/null +++ b/doc/workflow/importing/README.md @@ -0,0 +1,9 @@ +# Migrating projects to a GitLab instance
+
+1. [Bitbucket](import_projects_from_bitbucket.md)
+2. [GitHub](import_projects_from_github.md)
+3. [GitLab.com](import_projects_from_gitlab_com.md)
+4. [SVN](migrating_from_svn.md)
+
+### Note
+* If you'd like to migrate from a self-hosted GitLab instance to GitLab.com, you can copy your repos by changing the remote and pushing to the new server; but issues and merge requests can't be imported.
\ No newline at end of file diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png Binary files differnew file mode 100644 index 00000000000..df55a081803 --- /dev/null +++ b/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png Binary files differnew file mode 100644 index 00000000000..5253889d251 --- /dev/null +++ b/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png Binary files differnew file mode 100644 index 00000000000..ffa87ce5b2e --- /dev/null +++ b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png Binary files differnew file mode 100644 index 00000000000..0e08703f421 --- /dev/null +++ b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png diff --git a/doc/workflow/importing/github_importer/importer.png b/doc/workflow/importing/github_importer/importer.png Binary files differnew file mode 100644 index 00000000000..57636717571 --- /dev/null +++ b/doc/workflow/importing/github_importer/importer.png diff --git a/doc/workflow/importing/github_importer/new_project_page.png b/doc/workflow/importing/github_importer/new_project_page.png Binary files differnew file mode 100644 index 00000000000..002f22d81d7 --- /dev/null +++ b/doc/workflow/importing/github_importer/new_project_page.png diff --git a/doc/workflow/importing/gitlab_importer/importer.png b/doc/workflow/importing/gitlab_importer/importer.png Binary files differnew file mode 100644 index 00000000000..d2a286d8cac --- /dev/null +++ b/doc/workflow/importing/gitlab_importer/importer.png diff --git a/doc/workflow/importing/gitlab_importer/new_project_page.png b/doc/workflow/importing/gitlab_importer/new_project_page.png Binary files differnew file mode 100644 index 00000000000..5e239208e1e --- /dev/null +++ b/doc/workflow/importing/gitlab_importer/new_project_page.png diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md new file mode 100644 index 00000000000..1e9825e2e10 --- /dev/null +++ b/doc/workflow/importing/import_projects_from_bitbucket.md @@ -0,0 +1,26 @@ +# Import your project from Bitbucket to GitLab
+
+It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](doc/integration/bitbucket.md).
+
+* Sign in to GitLab.com and go to your dashboard
+
+* Click on "New project"
+
+
+
+* Click on the "Bitbucket" button
+
+
+
+* Grant GitLab access to your Bitbucket account
+
+
+
+* Click on the projects that you'd like to import or "Import all projects"
+
+
+
+A new GitLab project will be created with your imported data.
+
+### Note
+Milestones and wiki pages are not imported from Bitbucket.
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md new file mode 100644 index 00000000000..aad2c63817d --- /dev/null +++ b/doc/workflow/importing/import_projects_from_github.md @@ -0,0 +1,18 @@ +# Import your project from GitHub to GitLab
+
+It takes just a couple of steps to import your existing GitHub projects to GitLab. Keep in mind that it is possible only if
+GitHub support is enabled on your GitLab instance. You can read more about GitHub support [here](http://doc.gitlab.com/ce/integration/github.html)
+
+* Sign in to GitLab.com and go to your dashboard.
+* To get to the importer page, you need to go to the "New project" page.
+
+
+
+* Click on the "Import project from GitHub" link and you will be redirected to GitHub for permission to access your projects. After accepting, you'll be automatically redirected to the importer.
+
+
+
+* To import a project, you can simple click "Add". The importer will import your repository and issues. Once the importer is done, a new GitLab project will be created with your imported data.
+
+### Note
+When you import your projects from GitHub, it is not possible to keep your labels and milestones and issue numbers won't match. We are working on improving this in the near future.
\ No newline at end of file diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md new file mode 100644 index 00000000000..f4c4e955d46 --- /dev/null +++ b/doc/workflow/importing/import_projects_from_gitlab_com.md @@ -0,0 +1,18 @@ +# Project importing from GitLab.com to your private GitLab instance + +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) +To get to the importer page you need to go to "New project" page. + + + +Click on the "Import projects from Gitlab.com" link and you will be redirected to GitLab.com +for permission to access your projects. After accepting, you'll be automatically redirected to the importer. + + + + + +To import a project, you can simple click "Import". The importer will import your repository and issues. +Once the importer is done, a new GitLab project will be created with your imported data.
\ No newline at end of file diff --git a/doc/workflow/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md index 207e3641802..485db4834e9 100644 --- a/doc/workflow/migrating_from_svn.md +++ b/doc/workflow/importing/migrating_from_svn.md @@ -3,7 +3,7 @@ SVN stands for Subversion and is a version control system (VCS). Git is a distributed version control system. -There are some major differences between the two, for more information consult your favourite search engine. +There are some major differences between the two, for more information consult your favorite search engine. Git has tools for migrating SVN repositories to git, namely `git svn`. You can read more about this at [git documentation pages](http://git-scm.com/book/en/Git-and-Other-Systems-Git-and-Subversion). diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index 3c3ce162df5..17215de677e 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -24,14 +24,14 @@ Each of these settings have levels of notification: #### Global Settings Global Settings are at the bottom of the hierarchy. -Any setting set here will be overriden by a setting at the group or a project level. +Any setting set here will be overridden by a setting at the group or a project level. Group or Project settings can use `global` notification setting which will then use anything that is set at Global Settings. #### Group Settings -Group Settings are taking presedence over Global Settings but are on a level below Project Settings. +Group Settings are taking precedence over Global Settings but are on a level below Project Settings. This means that you can set a different level of notifications per group while still being able to have a finer level setting per project. Organization like this is suitable for users that belong to different groups but don't have the @@ -39,7 +39,7 @@ same need for being notified for every group they are member of. #### Project Settings -Project Settings are at the top level and any setting placed at this level will take presedence of any +Project Settings are at the top level and any setting placed at this level will take precedence of any other setting. This is suitable for users that have different needs for notifications per project basis. diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md new file mode 100644 index 00000000000..0adf9f8e3e8 --- /dev/null +++ b/doc/workflow/protected_branches.md @@ -0,0 +1,31 @@ +# Protected branches + +Permission in GitLab are fundamentally defined around the idea of having read or write permission to the repository and branches. + +To prevent people from messing with history or pushing code without review, we've created protected branches. + +A protected branch does three simple things: + +* it prevents pushes from everybody except users with Master permission +* it prevents anyone from force pushing to the branch +* it prevents anyone from deleting the branch + +You can make any branch a protected branch. GitLab makes the master branch a protected branch by default. + +To protect a branch, user needs to have at least a Master permission level, see [permissions document](doc/permissions/permissions.md). + + + +Navigate to project settings page and select `protected branches`. From the `Branch` dropdown menu select the branch you want to protect. + +Some workflows, like [GitLab workflow](gitlab_flow.md), require all users with write access to submit a Merge request in order to get the code into a protected branch. + +Since Masters and Owners can already push to protected branches, that means Developers cannot push to protected branch and need to submit a Merge request. + +However, there are workflows where that is not needed and only protecting from force pushes and branch removal is useful. + +For those workflows, you can allow everyone with write access to push to a protected branch by selecting `Developers can push` check box. + +On already protected branches you can also allow developers to push to the repository by selecting the `Developers can push` check box. + +
\ No newline at end of file diff --git a/doc/workflow/protected_branches/protected_branches1.png b/doc/workflow/protected_branches/protected_branches1.png Binary files differnew file mode 100644 index 00000000000..5c2a3de5f70 --- /dev/null +++ b/doc/workflow/protected_branches/protected_branches1.png diff --git a/doc/workflow/protected_branches/protected_branches2.png b/doc/workflow/protected_branches/protected_branches2.png Binary files differnew file mode 100644 index 00000000000..2dca3541365 --- /dev/null +++ b/doc/workflow/protected_branches/protected_branches2.png diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md new file mode 100644 index 00000000000..ffcb832cdd7 --- /dev/null +++ b/doc/workflow/shortcuts.md @@ -0,0 +1,5 @@ +# GitLab keyboard shortcuts + +You can see GitLab's keyboard shortcuts by using 'shift + ?' + +
\ No newline at end of file diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png Binary files differnew file mode 100644 index 00000000000..68756ed1f98 --- /dev/null +++ b/doc/workflow/shortcuts.png diff --git a/doc/workflow/timezone.md b/doc/workflow/timezone.md new file mode 100644 index 00000000000..7e08c0e51ac --- /dev/null +++ b/doc/workflow/timezone.md @@ -0,0 +1,30 @@ +# Changing your time zone + +The global time zone configuration parameter can be changed in `config/gitlab.yml`: +``` + # time_zone: 'UTC' +``` + +Uncomment and customize if you want to change the default time zone of GitLab application. + +To see all available time zones, run `bundle exec rake time:zones:all`. + + +## Changing time zone in omnibus installations + +GitLab defaults its time zone to UTC. It has a global timezone configuration parameter in `/etc/gitlab/gitlab.rb`. + +To update, add the time zone that best applies to your location. Here are two examples: +``` +gitlab_rails['time_zone'] = 'America/New_York' +``` +or +``` +gitlab_rails['time_zone'] = 'Europe/Brussels' +``` + +After you added this field, reconfigure and restart: +``` +gitlab-ctl reconfigure +gitlab-ctl restart +``` diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md new file mode 100644 index 00000000000..7fc8f96b9ec --- /dev/null +++ b/doc/workflow/web_editor.md @@ -0,0 +1,26 @@ +# GitLab Web Editor + +In GitLab you can create new files and edit existing files using our web editor. +This is especially useful if you don't have access to a command line or you just want to do a quick fix. +You can easily access the web editor, depending on the context. +Let's start from newly created project. + +Click on `Add a file` +to create the first file and open it in the web editor. + + + +Fill in a file name, some content, a commit message, branch name and press the commit button. +The file will be saved to the repository. + + + +You can edit any text file in a repository by pressing the edit button, when +viewing the file. + + + +Editing a file is almost the same as creating a new file, +with as addition the ability to preview your changes in a separate tab. Also you can save your change to another branch by filling out field `branch` + + diff --git a/doc/workflow/web_editor/edit_file.png b/doc/workflow/web_editor/edit_file.png Binary files differnew file mode 100644 index 00000000000..f480c69ac3e --- /dev/null +++ b/doc/workflow/web_editor/edit_file.png diff --git a/doc/workflow/web_editor/empty_project.png b/doc/workflow/web_editor/empty_project.png Binary files differnew file mode 100644 index 00000000000..6a049f6beaf --- /dev/null +++ b/doc/workflow/web_editor/empty_project.png diff --git a/doc/workflow/web_editor/new_file.png b/doc/workflow/web_editor/new_file.png Binary files differnew file mode 100644 index 00000000000..55ebd9e0257 --- /dev/null +++ b/doc/workflow/web_editor/new_file.png diff --git a/doc/workflow/web_editor/show_file.png b/doc/workflow/web_editor/show_file.png Binary files differnew file mode 100644 index 00000000000..9cafcb55109 --- /dev/null +++ b/doc/workflow/web_editor/show_file.png diff --git a/doc/workflow/wip_merge_requests.md b/doc/workflow/wip_merge_requests.md new file mode 100644 index 00000000000..46035a5e6b6 --- /dev/null +++ b/doc/workflow/wip_merge_requests.md @@ -0,0 +1,13 @@ +# "Work In Progress" Merge Requests + +To prevent merge requests from accidentally being accepted before they're completely ready, GitLab blocks the "Accept" button for merge requests that have been marked a **Work In Progress**. + + + +To mark a merge request a Work In Progress, simply start its title with `[WIP]` or `WIP:`. + + + +To allow a Work In Progress merge request to be accepted again when it's ready, simply remove the `WIP` prefix. + + diff --git a/doc/workflow/wip_merge_requests/blocked_accept_button.png b/doc/workflow/wip_merge_requests/blocked_accept_button.png Binary files differnew file mode 100644 index 00000000000..4791e5de972 --- /dev/null +++ b/doc/workflow/wip_merge_requests/blocked_accept_button.png diff --git a/doc/workflow/wip_merge_requests/mark_as_wip.png b/doc/workflow/wip_merge_requests/mark_as_wip.png Binary files differnew file mode 100644 index 00000000000..8fa83a201ac --- /dev/null +++ b/doc/workflow/wip_merge_requests/mark_as_wip.png diff --git a/doc/workflow/wip_merge_requests/unmark_as_wip.png b/doc/workflow/wip_merge_requests/unmark_as_wip.png Binary files differnew file mode 100644 index 00000000000..d45e68f31c5 --- /dev/null +++ b/doc/workflow/wip_merge_requests/unmark_as_wip.png diff --git a/doc/workflow/workflow.md b/doc/workflow/workflow.md index ab29cfb670b..f70e41df842 100644 --- a/doc/workflow/workflow.md +++ b/doc/workflow/workflow.md @@ -1,4 +1,4 @@ -# Workflow +# Feature branch workflow 1. Clone project: diff --git a/doc_styleguide.md b/doc_styleguide.md new file mode 100644 index 00000000000..db30a737f14 --- /dev/null +++ b/doc_styleguide.md @@ -0,0 +1,23 @@ +# Documentation styleguide + +This styleguide recommends best practices to improve documentation and to keep it organized and easy to find. + +## Text + +* Make sure that the documentation is added in the correct directory and that there's a link to it somewhere useful. + +* Add only one H1 or title in each document, by adding '#' at the begining of it (when using markdown). For subtitles, use '##', '###' and so on. + +* Do not duplicate information. + +* Be brief and clear. + +* Whenever it applies, add documents in alphabetical order. + +## When adding images to a document + +* Create a directory to store the images with the specific name of the document where the images belong. It could be in the same directory where the .md document that you're working on is located. + +* Images should have a specific, non-generic name that will differentiate them. + +* Keep all file names in lower case.
\ No newline at end of file diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 00000000000..dd449725e18 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1 @@ +*.md diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index b1720e15114..00000000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -# Data: docker run --name gitlab_data genezys/gitlab:7.5.1 /bin/true -# Run: docker run --detach --name gitlab --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data genezys/gitlab:7.5.1 - -FROM ubuntu:14.04 -MAINTAINER Vincent Robert <vincent.robert@genezys.net> - -# Install required packages -RUN apt-get update -q \ - && DEBIAN_FRONTEND=noninteractive apt-get install -qy \ - openssh-server \ - wget \ - && apt-get clean - -# Download & Install GitLab -RUN TMP_FILE=$(mktemp); \ - wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.5.1-omnibus.5.2.0.ci-1_amd64.deb \ - && dpkg -i $TMP_FILE \ - && rm -f $TMP_FILE - -# Manage SSHD through runit -RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ - && mkfifo /opt/gitlab/sv/sshd/supervise/ok \ - && printf "#!/bin/sh\nexec 2>&1\numask 077\nexec /usr/sbin/sshd -D" > /opt/gitlab/sv/sshd/run \ - && chmod a+x /opt/gitlab/sv/sshd/run \ - && ln -s /opt/gitlab/sv/sshd /opt/gitlab/service \ - && mkdir -p /var/run/sshd - -# Expose web & ssh -EXPOSE 80 22 - -# Volume & configuration -VOLUME ["/var/opt/gitlab", "/var/log/gitlab", "/etc/gitlab"] -ADD gitlab.rb /etc/gitlab/ - -# Default is to run runit & reconfigure -CMD gitlab-ctl reconfigure > /var/log/gitlab/reconfigure.log & /opt/gitlab/embedded/bin/runsvdir-start diff --git a/docker/README.md b/docker/README.md index ca56a9b35a4..fb3bde5016d 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,42 +1,162 @@ -What is GitLab? -=============== +# GitLab Docker images -GitLab offers git repository management, code reviews, issue tracking, activity feeds, wikis. It has LDAP/AD integration, handles 25,000 users on a single server but can also run on a highly available active/active cluster. A subscription gives you access to our support team and to GitLab Enterprise Edition that contains extra features aimed at larger organizations. +## What is GitLab? -<https://about.gitlab.com> +GitLab offers git repository management, code reviews, issue tracking, activity feeds, wikis. It has LDAP/AD integration, handles 25,000 users on a single server but can also run on a highly available active/active cluster. +Learn more on [https://about.gitlab.com](https://about.gitlab.com) - +## After starting a container +After starting a container you can go to [http://localhost:8080/](http://localhost:8080/) or [http://192.168.59.103:8080/](http://192.168.59.103:8080/) if you use boot2docker. -How to use this image. -====================== +It might take a while before the docker container is responding to queries. -I recommend creating a data volume container first, this will simplify migrations and backups: +You can check the status with something like `sudo docker logs -f 7c10172d7705`. - docker run --name gitlab_data genezys/gitlab:7.5.1 /bin/true +You can login to the web interface with username `root` and password `5iveL!fe`. -This empty container will exist to persist as volumes the 3 directories used by GitLab, so remember not to delete it: +Next time, you can just use docker start and stop to run the container. + +## How to build the docker images + +This guide will also let you know how to build docker images yourself. +Please run all the commands from the GitLab repo root directory. +People using boot2docker should run all the commands without sudo. + +## Choosing between the single and the app and data images + +Normally docker uses a single image for one applications. +But GitLab stores repositories and uploads in the filesystem. +This means that upgrades of a single image are hard. +That is why we recommend using separate app and data images. +We'll first describe how to use a single image. +After that we'll describe how to use the app and data images. + +## Single image + +Get a published image from Dockerhub: + +```bash +sudo docker pull sytse/gitlab-ce:7.10.1 +``` + +Run the image: + +```bash +sudo docker run --detach --publish 8080:80 --publish 2222:22 sytse/gitlab-ce:7.10.1 +``` + +After this you can login to the web interface as explained above in 'After starting a container'. + +Build the image: + +```bash +sudo docker build --tag sytse/gitlab-ce:7.10.1 docker/single/ +``` + +Publish the image to Dockerhub: + +```bash +sudo docker push sytse/gitlab-ce +``` + +Diagnosing commands: + +```bash +sudo docker run -i -t sytse/gitlab-ce:7.10.1 +sudo docker run -ti -e TERM=linux --name gitlab-ce-troubleshoot --publish 8080:80 --publish 2222:22 sytse/gitlab-ce:7.10.1 bash /usr/local/bin/wrapper +``` + +## App and data images + +### Get published images from Dockerhub + +```bash +sudo docker pull sytse/gitlab-data +sudo docker pull sytse/gitlab-app:7.10.1 +``` + +### Run the images + +```bash +sudo docker run --name gitlab-data sytse/gitlab-data /bin/true +sudo docker run --detach --name gitlab-app --publish 8080:80 --publish 2222:22 --volumes-from gitlab-data sytse/gitlab-app:7.10.1 +``` + +After this you can login to the web interface as explained above in 'After starting a container'. + +### Build images + +Build your own based on the Omnibus packages with the following commands. + +```bash +sudo docker build --tag gitlab-data docker/data/ +sudo docker build --tag gitlab-app:7.10.1 docker/app/ +``` + +After this run the images: + +```bash +sudo docker run --name gitlab-data gitlab-data /bin/true +sudo docker run --detach --name gitlab-app --publish 8080:80 --publish 2222:22 --volumes-from gitlab-data gitlab-app:7.10.1 +``` + +We assume using a data volume container, this will simplify migrations and backups. +This empty container will exist to persist as volumes the 3 directories used by GitLab, so remember not to delete it. + +The directories on data container are: - `/var/opt/gitlab` for application data - `/var/log/gitlab` for logs - `/etc/gitlab` for configuration -Then run GitLab: +### Configure GitLab + +This container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`. - docker run --detach --name gitlab --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data genezys/gitlab:7.5.1 +To access GitLab configuration, you can start an interactive command line in a new container using the shared data volume container, you will be able to browse the 3 directories and use your favorite text editor: -You can then go to `http://localhost:8080/` (or most likely `http://192.168.59.103:8080/` if you use boot2docker). Next time, you can just use `docker start gitlab` and `docker stop gitlab`. +```bash +sudo docker run -ti -e TERM=linux --rm --volumes-from gitlab-data ubuntu +vi /etc/gitlab/gitlab.rb +``` +**Note** that GitLab will reconfigure itself **at each container start.** You will need to restart the container to reconfigure your GitLab. -How to configure GitLab. -======================== +You can find all available options in [Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration). -This container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`. +### Upgrade GitLab with app and data images -To access GitLab configuration, you can start a new container using the shared data volume container: +To upgrade GitLab to new versions, stop running container, create new docker image and container from that image. - docker run -ti --rm --volumes-from gitlab_data ubuntu vi /etc/gitlab/gitlab.rb +It Assumes that you're upgrading from 7.8.1 to 7.10.1 and you're in the updated GitLab repo root directory: -**Note** that GitLab will reconfigure itself **at each container start.** You will need to restart the container to reconfigure your GitLab. +```bash +sudo docker stop gitlab-app +sudo docker rm gitlab-app +sudo docker build --tag gitlab-app:7.10.1 docker/app/ +sudo docker run --detach --name gitlab-app --publish 8080:80 --publish 2222:22 --volumes-from gitlab-data gitlab-app:7.10.1 +``` + +On the first run GitLab will reconfigure and update itself. If everything runs OK don't forget to cleanup the app image: + +```bash +sudo docker rmi gitlab-app:7.8.1 +``` + +### Publish images to Dockerhub + +- Ensure the containers are running +- Login to Dockerhub with `sudo docker login` +- Run the following (replace '7.10.1' with the version you're using and 'Sytse Sijbrandij' with your name): + +```bash +sudo docker commit -m "Initial commit" -a "Sytse Sijbrandij" gitlab-app sytse/gitlab-app:7.10.1 +sudo docker push sytse/gitlab-app:7.10.1 +sudo docker commit -m "Initial commit" -a "Sytse Sijbrandij" gitlab-data sytse/gitlab-data +sudo docker push sytse/gitlab-data +``` + +## Troubleshooting -You can find all available options in [GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration). +Please see the [troubleshooting](troubleshooting.md) file in this directory.
\ No newline at end of file diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile new file mode 100644 index 00000000000..fe3f7f0bcd2 --- /dev/null +++ b/docker/app/Dockerfile @@ -0,0 +1,32 @@ +FROM ubuntu:14.04 + +# Install required packages +RUN apt-get update -q \ + && DEBIAN_FRONTEND=noninteractive apt-get install -qy --no-install-recommends \ + ca-certificates \ + openssh-server \ + wget \ + apt-transport-https + +# Download & Install GitLab +# If you run GitLab Enterprise Edition point it to a location where you have downloaded it. +RUN echo "deb https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/ `lsb_release -cs` main" > /etc/apt/sources.list.d/gitlab_gitlab-ce.list +RUN wget -q -O - https://packages.gitlab.com/gpg.key | apt-key add - +RUN apt-get update && apt-get install -yq --no-install-recommends gitlab-ce + +# Manage SSHD through runit +RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ + && mkfifo /opt/gitlab/sv/sshd/supervise/ok \ + && printf "#!/bin/sh\nexec 2>&1\numask 077\nexec /usr/sbin/sshd -D" > /opt/gitlab/sv/sshd/run \ + && chmod a+x /opt/gitlab/sv/sshd/run \ + && ln -s /opt/gitlab/sv/sshd /opt/gitlab/service \ + && mkdir -p /var/run/sshd + +# Expose web & ssh +EXPOSE 80 22 + +# Copy assets +COPY assets/wrapper /usr/local/bin/ + +# Wrapper to handle signal, trigger runit and reconfigure GitLab +CMD ["/usr/local/bin/wrapper"]
\ No newline at end of file diff --git a/docker/app/assets/wrapper b/docker/app/assets/wrapper new file mode 100755 index 00000000000..9e6e7a05903 --- /dev/null +++ b/docker/app/assets/wrapper @@ -0,0 +1,17 @@ +#!/bin/bash + +function sigterm_handler() { + echo "SIGTERM signal received, try to gracefully shutdown all services..." + gitlab-ctl stop +} + +trap "sigterm_handler; exit" TERM + +function entrypoint() { + # Default is to run runit and reconfigure GitLab + gitlab-ctl reconfigure & + /opt/gitlab/embedded/bin/runsvdir-start & + wait +} + +entrypoint diff --git a/docker/data/Dockerfile b/docker/data/Dockerfile new file mode 100644 index 00000000000..ea0175c4aa2 --- /dev/null +++ b/docker/data/Dockerfile @@ -0,0 +1,8 @@ +FROM busybox + +# Declare volumes +VOLUME ["/var/opt/gitlab", "/var/log/gitlab", "/etc/gitlab"] +# Copy assets +COPY assets/gitlab.rb /etc/gitlab/ + +CMD /bin/sh diff --git a/docker/gitlab.rb b/docker/data/assets/gitlab.rb index da909db01f8..7fddf309c01 100644 --- a/docker/gitlab.rb +++ b/docker/data/assets/gitlab.rb @@ -4,6 +4,12 @@ # even if you intend to use another port in Docker. external_url "http://192.168.59.103/" +# Prevent Postgres from trying to allocate 25% of total memory +postgresql['shared_buffers'] = '1MB' + +# Configure GitLab to redirect PostgreSQL logs to the data volume +postgresql['log_directory'] = '/var/log/gitlab/postgresql' + # Some configuration of GitLab # You can find more at https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration gitlab_rails['gitlab_email_from'] = 'gitlab@example.com' diff --git a/docker/single/Dockerfile b/docker/single/Dockerfile new file mode 100644 index 00000000000..a6cbf131237 --- /dev/null +++ b/docker/single/Dockerfile @@ -0,0 +1,34 @@ +FROM ubuntu:14.04 +MAINTAINER Sytse Sijbrandij + +# Install required packages +RUN apt-get update -q \ + && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ + ca-certificates \ + openssh-server \ + wget \ + apt-transport-https + +# Download & Install GitLab +# If you run GitLab Enterprise Edition point it to a location where you have downloaded it. +RUN echo "deb https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/ `lsb_release -cs` main" > /etc/apt/sources.list.d/gitlab_gitlab-ce.list +RUN wget -q -O - https://packages.gitlab.com/gpg.key | apt-key add - +RUN apt-get update && apt-get install -yq --no-install-recommends gitlab-ce + +# Manage SSHD through runit +RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ + && mkfifo /opt/gitlab/sv/sshd/supervise/ok \ + && printf "#!/bin/sh\nexec 2>&1\numask 077\nexec /usr/sbin/sshd -D" > /opt/gitlab/sv/sshd/run \ + && chmod a+x /opt/gitlab/sv/sshd/run \ + && ln -s /opt/gitlab/sv/sshd /opt/gitlab/service \ + && mkdir -p /var/run/sshd + +# Expose web & ssh +EXPOSE 80 22 + +# Copy assets +COPY assets/wrapper /usr/local/bin/ +COPY assets/gitlab.rb /etc/gitlab/ + +# Wrapper to handle signal, trigger runit and reconfigure GitLab +CMD ["/usr/local/bin/wrapper"] diff --git a/docker/single/assets/gitlab.rb b/docker/single/assets/gitlab.rb new file mode 100644 index 00000000000..ef84e7832d6 --- /dev/null +++ b/docker/single/assets/gitlab.rb @@ -0,0 +1,37 @@ +# External URL should be your Docker instance. +# By default, GitLab will use the Docker container hostname. +# Always use port 80 here to force the internal nginx to bind port 80, +# even if you intend to use another port in Docker. +# external_url "http://192.168.59.103/" + +# Prevent Postgres from trying to allocate 25% of total memory +postgresql['shared_buffers'] = '1MB' + +# Configure GitLab to redirect PostgreSQL logs to the data volume +postgresql['log_directory'] = '/var/log/gitlab/postgresql' + +# Some configuration of GitLab +# You can find more at https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration +gitlab_rails['gitlab_email_from'] = 'gitlab@example.com' +gitlab_rails['gitlab_support_email'] = 'support@example.com' +gitlab_rails['time_zone'] = 'Europe/Paris' + +# SMTP settings +# You must use an external server, the Docker container does not install an SMTP server +gitlab_rails['smtp_enable'] = true +gitlab_rails['smtp_address'] = "smtp.example.com" +gitlab_rails['smtp_port'] = 587 +gitlab_rails['smtp_user_name'] = "user" +gitlab_rails['smtp_password'] = "password" +gitlab_rails['smtp_domain'] = "example.com" +gitlab_rails['smtp_authentication'] = "plain" +gitlab_rails['smtp_enable_starttls_auto'] = true + +# Enable LDAP authentication +# gitlab_rails['ldap_enabled'] = true +# gitlab_rails['ldap_host'] = 'ldap.example.com' +# gitlab_rails['ldap_port'] = 389 +# gitlab_rails['ldap_method'] = 'plain' # 'ssl' or 'plain' +# gitlab_rails['ldap_allow_username_or_email_login'] = false +# gitlab_rails['ldap_uid'] = 'uid' +# gitlab_rails['ldap_base'] = 'ou=users,dc=example,dc=com' diff --git a/docker/single/assets/wrapper b/docker/single/assets/wrapper new file mode 100755 index 00000000000..966b2cab4a1 --- /dev/null +++ b/docker/single/assets/wrapper @@ -0,0 +1,16 @@ +#!/bin/bash + +function sigterm_handler() { + echo "SIGTERM signal received, try to gracefully shutdown all services..." + gitlab-ctl stop +} + +trap "sigterm_handler; exit" TERM + +function entrypoint() { + /opt/gitlab/embedded/bin/runsvdir-start & + gitlab-ctl reconfigure # will also start everything + gitlab-ctl tail # tail all logs +} + +entrypoint diff --git a/docker/single/marathon.json b/docker/single/marathon.json new file mode 100644 index 00000000000..d23c2b84e0e --- /dev/null +++ b/docker/single/marathon.json @@ -0,0 +1,14 @@ +{ + "id": "/gitlab", + "ports": [0,0], + "cpus": 2, + "mem": 2048.0, + "disk": 10240.0, + "container": { + "type": "DOCKER", + "docker": { + "network": "HOST", + "image": "sytse/gitlab-ce:7.10.1" + } + } +}
\ No newline at end of file diff --git a/docker/troubleshooting.md b/docker/troubleshooting.md new file mode 100644 index 00000000000..5827f2185db --- /dev/null +++ b/docker/troubleshooting.md @@ -0,0 +1,73 @@ +# Troubleshooting + +This is to troubleshoot https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/245 +But it might contain useful commands for other cases as well. + +The configuration to add the postgres log in vim is: +postgresql['log_directory'] = '/var/log/gitlab/postgresql' + +# Commands + +```bash +sudo docker build --tag gitlab_image docker/ + +sudo docker rm -f gitlab_app +sudo docker rm -f gitlab_data + +sudo docker run --name gitlab_data gitlab_image /bin/true + +sudo docker run -ti --rm --volumes-from gitlab_data ubuntu apt-get update && sudo apt-get install -y vim && sudo vim /etc/gitlab/gitlab.rb + +sudo docker run --detach --name gitlab_app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_image + +sudo docker run -t --rm --volumes-from gitlab_data ubuntu tail -f /var/log/gitlab/reconfigure.log + +sudo docker run -t --rm --volumes-from gitlab_data ubuntu tail -f /var/log/gitlab/postgresql/current + +sudo docker run -t --rm --volumes-from gitlab_data ubuntu cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers + +sudo docker run -t --rm --volumes-from gitlab_data ubuntu cat /etc/gitlab/gitlab.rb +``` + +# Interactively + +```bash +# First start a GitLab container without starting GitLab +# This is almost the same as starting the GitLab container except: +# - we run interactively (-t -i) +# - we define TERM=linux because it allows to use arrow keys in vi (!!!) +# - we choose another startup command (bash) +sudo docker run -ti -e TERM=linux --name gitlab_app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_image bash + +# Configure GitLab to redirect PostgreSQL logs +echo "postgresql['log_directory'] = '/var/log/gitlab/postgresql'" >> /etc/gitlab/gitlab.rb + +# Prevent Postgres from allocating 25% of total memory +echo "postgresql['shared_buffers'] = '1MB'" >> /etc/gitlab/gitlab.rb + +# You can now start GitLab manually from Bash (in the background) +# Maybe the command below is still missing something to run in the background +gitlab-ctl reconfigure > /var/log/gitlab/reconfigure.log & /opt/gitlab/embedded/bin/runsvdir-start & + +# Inspect PostgreSQL config +cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers + +# And tail the logs (PostgreSQL log may not exist immediately) +tail -f /var/log/gitlab/reconfigure.log /var/log/gitlab/postgresql/current + +# And get the memory +cat /proc/meminfo +head /proc/sys/kernel/shmmax /proc/sys/kernel/shmall +free -m + +``` + +# Cleanup + +Remove ALL docker containers and images (also non GitLab ones): + +``` +docker rm $(docker ps -a -q) +docker rmi $(docker images -q) +``` + diff --git a/features/admin/applications.feature b/features/admin/applications.feature new file mode 100644 index 00000000000..2a00e1666c0 --- /dev/null +++ b/features/admin/applications.feature @@ -0,0 +1,18 @@ +@admin +Feature: Admin Applications + Background: + Given I sign in as an admin + And I visit applications page + + Scenario: I can manage application + Then I click on new application button + And I should see application form + Then I fill application form out and submit + And I see application + Then I click edit + And I see edit application form + Then I change name of application and submit + And I see that application was changed + Then I visit applications page + And I click to remove application + Then I see that application is removed
\ No newline at end of file diff --git a/features/admin/deploy_keys.feature b/features/admin/deploy_keys.feature new file mode 100644 index 00000000000..33439cd1e85 --- /dev/null +++ b/features/admin/deploy_keys.feature @@ -0,0 +1,16 @@ +@admin +Feature: Admin Deploy Keys + Background: + Given I sign in as an admin + And there are public deploy keys in system + + Scenario: Deploy Keys list + When I visit admin deploy keys page + Then I should see all public deploy keys + + Scenario: Deploy Keys new + When I visit admin deploy keys page + And I click 'New Deploy Key' + And I submit new deploy key + Then I should be on admin deploy keys page + And I should see newly created deploy key diff --git a/features/admin/settings.feature b/features/admin/settings.feature new file mode 100644 index 00000000000..e38eea2cfed --- /dev/null +++ b/features/admin/settings.feature @@ -0,0 +1,19 @@ +@admin +Feature: Admin Settings + Background: + Given I sign in as an admin + And I visit admin settings page + + Scenario: Change application settings + When I modify settings and save form + Then I should see application settings saved + + Scenario: Change Slack Service Template settings + When I click on "Service Templates" + And I click on "Slack" service + And I fill out Slack settings + Then I check all events and submit form + And I should see service template settings saved + Then I click on "Slack" service + And I should see all checkboxes checked + And I should see Slack settings saved diff --git a/features/admin/users.feature b/features/admin/users.feature index 278f6a43e94..1a8720dd77e 100644 --- a/features/admin/users.feature +++ b/features/admin/users.feature @@ -35,3 +35,13 @@ Feature: Admin Users And I see the secondary email When I click remove secondary email Then I should not see secondary email anymore + + Scenario: Show user keys + Given user "Pete" with ssh keys + And I visit admin users page + And click on user "Pete" + Then I should see key list + And I click on the key title + Then I should see key details + And I click on remove key + Then I should see the key removed diff --git a/features/dashboard/archived_projects.feature b/features/dashboard/archived_projects.feature index 3af93bc373c..69b3a776441 100644 --- a/features/dashboard/archived_projects.feature +++ b/features/dashboard/archived_projects.feature @@ -10,8 +10,3 @@ Feature: Dashboard Archived Projects Scenario: I should see non-archived projects on dashboard Then I should see "Shop" project link And I should not see "Forum" project link - - Scenario: I should see all projects on projects page - And I visit dashboard projects page - Then I should see "Shop" project link - And I should see "Forum" project link diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index bebaa78e46c..1959d327082 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -27,11 +27,11 @@ Feature: Dashboard Scenario: I should see User joined Project event Given user with name "John Doe" joined project "Shop" When I visit dashboard page - Then I should see "John Doe joined project at Shop" event + Then I should see "John Doe joined project Shop" event @javascript Scenario: I should see User left Project event Given user with name "John Doe" joined project "Shop" And user with name "John Doe" left project "Shop" When I visit dashboard page - Then I should see "John Doe left project at Shop" event + Then I should see "John Doe left project Shop" event diff --git a/features/profile/group.feature b/features/dashboard/group.feature index e2fbfde77be..e3c01db2ebb 100644 --- a/features/profile/group.feature +++ b/features/dashboard/group.feature @@ -1,5 +1,5 @@ -@profile -Feature: Profile Group +@dashboard +Feature: Dashboard Group Background: Given I sign in as "John Doe" And "John Doe" is owner of group "Owned" @@ -10,39 +10,48 @@ Feature: Profile Group @javascript Scenario: Owner should be able to leave from group if he is not the last owner Given "Mary Jane" is owner of group "Owned" - When I visit profile groups page + When I visit dashboard groups page Then I should see group "Owned" in group list Then I should see group "Guest" in group list When I click on the "Leave" button for group "Owned" - And I visit profile groups page + And I visit dashboard groups page Then I should not see group "Owned" in group list Then I should see group "Guest" in group list @javascript Scenario: Owner should not be able to leave from group if he is the last owner Given "Mary Jane" is guest of group "Owned" - When I visit profile groups page + When I visit dashboard groups page Then I should see group "Owned" in group list Then I should see group "Guest" in group list - Then I should not see the "Leave" button for group "Owned" + When I click on the "Leave" button for group "Owned" + Then I should see the "Can not leave message" @javascript Scenario: Guest should be able to leave from group Given "Mary Jane" is guest of group "Guest" - When I visit profile groups page + When I visit dashboard groups page Then I should see group "Owned" in group list Then I should see group "Guest" in group list When I click on the "Leave" button for group "Guest" - When I visit profile groups page + When I visit dashboard groups page Then I should see group "Owned" in group list Then I should not see group "Guest" in group list @javascript Scenario: Guest should be able to leave from group even if he is the only user in the group - When I visit profile groups page + When I visit dashboard groups page Then I should see group "Owned" in group list Then I should see group "Guest" in group list When I click on the "Leave" button for group "Guest" - When I visit profile groups page + When I visit dashboard groups page Then I should see group "Owned" in group list Then I should not see group "Guest" in group list + + Scenario: Create a group from dasboard + And I visit dashboard groups page + And I click new group link + And submit form with new group "Samurai" info + Then I should be redirected to group "Samurai" page + And I should see newly created group "Samurai" + diff --git a/features/dashboard/issues.feature b/features/dashboard/issues.feature index 72627e43e05..99dad88a402 100644 --- a/features/dashboard/issues.feature +++ b/features/dashboard/issues.feature @@ -10,10 +10,12 @@ Feature: Dashboard Issues Scenario: I should see assigned issues Then I should see issues assigned to me + @javascript Scenario: I should see authored issues When I click "Authored by me" link Then I should see issues authored by me + @javascript Scenario: I should see all issues When I click "All" link Then I should see all issues diff --git a/features/dashboard/merge_requests.feature b/features/dashboard/merge_requests.feature index dcef1290e7e..4a2c997d707 100644 --- a/features/dashboard/merge_requests.feature +++ b/features/dashboard/merge_requests.feature @@ -10,10 +10,12 @@ Feature: Dashboard Merge Requests Scenario: I should see assigned merge_requests Then I should see merge requests assigned to me + @javascript Scenario: I should see authored merge_requests When I click "Authored by me" link Then I should see merge requests authored by me + @javascript Scenario: I should see all merge_requests When I click "All" link Then I should see all merge requests diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature new file mode 100644 index 00000000000..431dc4ccfcb --- /dev/null +++ b/features/dashboard/new_project.feature @@ -0,0 +1,13 @@ +@dashboard +Feature: New Project +Background: + Given I sign in as a user + And I own project "Shop" + And I visit dashboard page + + @javascript + Scenario: I should see New projects page + Given I click "New project" link + Then I see "New project" page + When I click on "Import project from GitHub" + Then I see instructions on how to import from GitHub diff --git a/features/dashboard/projects.feature b/features/dashboard/projects.feature deleted file mode 100644 index bb4e84f0159..00000000000 --- a/features/dashboard/projects.feature +++ /dev/null @@ -1,9 +0,0 @@ -@dashboard -Feature: Dashboard Projects - Background: - Given I sign in as a user - And I own project "Shop" - And I visit dashboard projects page - - Scenario: I should see projects list - Then I should see projects list diff --git a/features/dashboard/starred_projects.feature b/features/dashboard/starred_projects.feature new file mode 100644 index 00000000000..9dfd2fbab9c --- /dev/null +++ b/features/dashboard/starred_projects.feature @@ -0,0 +1,12 @@ +@dashboard +Feature: Dashboard Starred Projects + Background: + Given I sign in as a user + And public project "Community" + And I starred project "Community" + And I own project "Shop" + And I visit dashboard starred projects page + + Scenario: I should see projects list + Then I should see project "Community" + And I should not see project "Shop" diff --git a/features/explore/groups.feature b/features/explore/groups.feature index b50a3e766c6..c11634bd74a 100644 --- a/features/explore/groups.feature +++ b/features/explore/groups.feature @@ -28,7 +28,6 @@ Feature: Explore Groups Given group "TestGroup" has internal project "Internal" When I sign in as a user And I visit group "TestGroup" issues page - And I change filter to Everyone's Then I should see project "Internal" items And I should not see project "Enterprise" items @@ -36,7 +35,6 @@ Feature: Explore Groups Given group "TestGroup" has internal project "Internal" When I sign in as a user And I visit group "TestGroup" merge requests page - And I change filter to Everyone's Then I should see project "Internal" items And I should not see project "Enterprise" items @@ -94,7 +92,6 @@ Feature: Explore Groups Given group "TestGroup" has public project "Community" When I sign in as a user And I visit group "TestGroup" issues page - And I change filter to Everyone's Then I should see project "Community" items And I should see project "Internal" items And I should not see project "Enterprise" items @@ -104,7 +101,6 @@ Feature: Explore Groups Given group "TestGroup" has public project "Community" When I sign in as a user And I visit group "TestGroup" merge requests page - And I change filter to Everyone's Then I should see project "Community" items And I should see project "Internal" items And I should not see project "Enterprise" items diff --git a/features/groups.feature b/features/groups.feature index b5ff03db844..415e43d6ae7 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -10,14 +10,6 @@ Feature: Groups Then I should see group "Owned" projects list And I should see projects activity feed - Scenario: Create a group from dasboard - When I visit group "Owned" page - And I visit dashboard page - And I click new group link - And submit form with new group "Samurai" info - Then I should be redirected to group "Samurai" page - And I should see newly created group "Samurai" - Scenario: I should see group "Owned" issues list Given project from group "Owned" has issues assigned to me When I visit group "Owned" issues page @@ -55,6 +47,21 @@ Feature: Groups Then I should not see group "Owned" avatar And I should not see the "Remove avatar" button + @javascript + Scenario: Add user to group + Given gitlab user "Mike" + When I visit group "Owned" members page + And I click link "Add members" + When I select "Mike" as "Reporter" + Then I should see "Mike" in team list as "Reporter" + + @javascript + Scenario: Invite user to group + When I visit group "Owned" members page + And I click link "Add members" + When I select "sjobs@apple.com" as "Reporter" + Then I should see "sjobs@apple.com" in team list as invited "Reporter" + # Leave @javascript diff --git a/features/invites.feature b/features/invites.feature new file mode 100644 index 00000000000..dc8eefaeaed --- /dev/null +++ b/features/invites.feature @@ -0,0 +1,45 @@ +Feature: Invites + Background: + Given "John Doe" is owner of group "Owned" + And "John Doe" has invited "user@example.com" to group "Owned" + + Scenario: Viewing invitation when signed out + When I visit the invitation page + Then I should be redirected to the sign in page + And I should see a notice telling me to sign in + + Scenario: Signing in to view invitation + When I visit the invitation page + And I sign in as "Mary Jane" + Then I should be redirected to the invitation page + + Scenario: Viewing invitation when signed in + Given I sign in as "Mary Jane" + And I visit the invitation page + Then I should see the invitation details + And I should see an "Accept invitation" button + And I should see a "Decline" button + + Scenario: Viewing invitation as an existing member + Given I sign in as "John Doe" + And I visit the invitation page + Then I should see a message telling me I'm already a member + + Scenario: Accepting the invitation + Given I sign in as "Mary Jane" + And I visit the invitation page + And I click the "Accept invitation" button + Then I should be redirected to the group page + And I should see a notice telling me I have access + + Scenario: Declining the application when signed in + Given I sign in as "Mary Jane" + And I visit the invitation page + And I click the "Decline" button + Then I should be redirected to the dashboard + And I should see a notice telling me I have declined + + Scenario: Declining the application when signed out + When I visit the invitation's decline page + Then I should be redirected to the sign in page + And I should see a notice telling me I have declined diff --git a/features/profile/active_tab.feature b/features/profile/active_tab.feature index 7801ae5b8ca..1fa4ac88ddc 100644 --- a/features/profile/active_tab.feature +++ b/features/profile/active_tab.feature @@ -18,9 +18,9 @@ Feature: Profile Active Tab Then the active main tab should be SSH Keys And no other main tabs should be active - Scenario: On Profile Design - Given I visit profile design page - Then the active main tab should be Design + Scenario: On Profile Preferences + Given I visit profile preferences page + Then the active main tab should be Preferences And no other main tabs should be active Scenario: On Profile History diff --git a/features/profile/profile.feature b/features/profile/profile.feature index d7fa370fe2a..0dd0afde8b1 100644 --- a/features/profile/profile.feature +++ b/features/profile/profile.feature @@ -71,34 +71,16 @@ Feature: Profile And I click on my profile picture Then I should see my user page - @javascript - Scenario: I change my application theme - Given I visit profile design page - When I change my application theme - Then I should see the theme change immediately - And I should receive feedback that the changes were saved - - @javascript - Scenario: I change my code preview theme - Given I visit profile design page - When I change my code preview theme - Then I should receive feedback that the changes were saved - - @javascript - Scenario: I see the password strength indicator - Given I visit profile password page - When I try to set a weak password - Then I should see the input field yellow - - @javascript - Scenario: I see the password strength indicator error - Given I visit profile password page - When I try to set a short password - Then I should see the input field red - And I should see the password error message - - @javascript - Scenario: I see the password strength indicator with success - Given I visit profile password page - When I try to set a strong password - Then I should see the input field green
\ No newline at end of file + Scenario: I can manage application + Given I visit profile applications page + Then I click on new application button + And I should see application form + Then I fill application form out and submit + And I see application + Then I click edit + And I see edit application form + Then I change name of application and submit + And I see that application was changed + Then I visit profile applications page + And I click to remove application + Then I see that application is removed diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index ed548177837..8661ea98c20 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -35,6 +35,11 @@ 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 +54,6 @@ Feature: Project Active Tab # Sub Tabs: Settings - Scenario: On Project Settings/Team - Given I visit my project's settings page - And I click the "Team" tab - Then the active sub nav should be Team - And no other sub navs should be active - And the active main tab should be Settings - Scenario: On Project Settings/Edit Given I visit my project's settings page And I click the "Edit" tab @@ -106,24 +104,19 @@ Feature: Project Active Tab And no other sub tabs should be active And the active main tab should be Commits - # Sub Tabs: Issues - Scenario: On Project Issues/Browse Given I visit my project's issues page - Then the active sub tab should be Issues - And no other sub tabs should be active - And the active main tab should be Issues + Then the active main tab should be Issues + And no other main tabs should be active Scenario: On Project Issues/Milestones Given I visit my project's issues page And I click the "Milestones" tab - Then the active sub tab should be Milestones - And no other sub tabs should be active - And the active main tab should be Issues + Then the active main tab should be Milestones + And no other main 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 sub tab should be Labels - And no other sub tabs should be active - And the active main tab should be Issues + Then the active main tab should be Labels + And no other main tabs should be active diff --git a/features/project/archived.feature b/features/project/archived.feature index 9aac29384ba..ad466f4f307 100644 --- a/features/project/archived.feature +++ b/features/project/archived.feature @@ -14,15 +14,6 @@ Feature: Project Archived And I visit project "Forum" page Then I should see "Archived" - Scenario: I should not see archived on projects page with no archived projects - And I visit dashboard projects page - Then I should not see "Archived" - - Scenario: I should see archived on projects page with archived projects - And project "Forum" is archived - And I visit dashboard projects page - Then I should see "Archived" - Scenario: I archive project When project "Shop" has push event And I visit project "Shop" page diff --git a/features/project/commits/comments.feature b/features/project/commits/comments.feature index e176752cfbf..320f008abb6 100644 --- a/features/project/commits/comments.feature +++ b/features/project/commits/comments.feature @@ -14,14 +14,9 @@ Feature: Project Commits Comments Then I should not see the cancel comment button @javascript - Scenario: I can't preview without text - Given I haven't written any comment text - Then I should not see the comment preview button - - @javascript Scenario: I can preview with text - Given I write a comment like "Nice" - Then I should see the comment preview button + Given I write a comment like ":+1: Nice" + Then The comment preview tab should be display rendered Markdown @javascript Scenario: I preview a comment @@ -32,7 +27,7 @@ Feature: Project Commits Comments @javascript Scenario: I can edit after preview Given I preview a comment text like "Bug fixed :smile:" - Then I should see the comment edit button + Then I should see the comment write tab @javascript Scenario: I have a reset form after posting from preview @@ -44,5 +39,12 @@ Feature: Project Commits Comments @javascript Scenario: I can delete a comment Given I leave a comment like "XML attached" + Then I should see a comment saying "XML attached" And I delete a comment Then I should not see a comment saying "XML attached" + + @javascript + Scenario: I can edit a comment with +1 + Given I leave a comment like "XML attached" + And I edit the last comment with a +1 + Then I should see +1 in the description diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index 46076b6f3e6..c4b206edc95 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -21,10 +21,13 @@ Feature: Project Commits And I click side-by-side diff button Then I see inline diff button + @javascript Scenario: I compare refs Given I visit compare refs page And I fill compare fields with refs Then I see compared refs + And I unfold diff + Then I should see additional file lines Scenario: I browse commits for a specific path Given I visit my project's commits page for a specific path diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature index a145ec84b78..4a2b870e082 100644 --- a/features/project/commits/diff_comments.feature +++ b/features/project/commits/diff_comments.feature @@ -55,16 +55,10 @@ Feature: Project Commits Diff Comments Then I should see a discussion reply button @javascript - Scenario: I can't preview without text - Given I open a diff comment form - And I haven't written any diff comment text - Then I should not see the diff comment preview button - - @javascript Scenario: I can preview with text Given I open a diff comment form And I write a diff comment like ":-1: I don't like this" - Then I should see the diff comment preview button + Then The diff comment preview tab should display rendered Markdown @javascript Scenario: I preview a diff comment @@ -75,7 +69,7 @@ Feature: Project Commits Diff Comments @javascript Scenario: I can edit after preview Given I preview a diff comment text like "Should fix it :smile:" - Then I should see the diff comment edit button + Then I should see the diff comment write tab @javascript Scenario: The form gets removed after posting @@ -83,3 +77,17 @@ Feature: Project Commits Diff Comments And I submit the diff comment Then I should not see the diff comment form And I should see a discussion reply button + + @javascript + Scenario: I can add a comment on a side-by-side commit diff (left side) + Given I open a diff comment form + And I click side-by-side diff button + When I leave a diff comment in a parallel view on the left side like "Old comment" + Then I should see a diff comment on the left side saying "Old comment" + + @javascript + Scenario: I can add a comment on a side-by-side commit diff (right side) + Given I open a diff comment form + And I click side-by-side diff button + When I leave a diff comment in a parallel view on the right side like "New comment" + Then I should see a diff comment on the right side saying "New comment" diff --git a/features/project/deploy_keys.feature b/features/project/deploy_keys.feature index 13e3b9bbd2e..47cf774094f 100644 --- a/features/project/deploy_keys.feature +++ b/features/project/deploy_keys.feature @@ -6,7 +6,18 @@ Feature: Project Deploy Keys Scenario: I should see deploy keys list Given project has deploy key When I visit project deploy keys page - Then I should see project deploy keys + Then I should see project deploy key + + Scenario: I should see project deploy keys + Given other projects have deploy keys + When I visit project deploy keys page + Then I should see other project deploy key + And I should only see the same deploy key once + + Scenario: I should see public deploy keys + Given public deploy key exists + When I visit project deploy keys page + Then I should see public deploy key Scenario: I add new deploy key Given I visit project deploy keys page @@ -15,8 +26,15 @@ Feature: Project Deploy Keys Then I should be on deploy keys page And I should see newly created deploy key - Scenario: I attach deploy key to project - Given other project has deploy key + Scenario: I attach other project deploy key to project + Given other projects have deploy keys + And I visit project deploy keys page + When I click attach deploy key + Then I should be on deploy keys page + And I should see newly created deploy key + + Scenario: I attach public deploy key to project + Given public deploy key exists And I visit project deploy keys page When I click attach deploy key Then I should be on deploy keys page diff --git a/features/project/edit_issuetracker.feature b/features/project/edit_issuetracker.feature deleted file mode 100644 index cc0de07ca69..00000000000 --- a/features/project/edit_issuetracker.feature +++ /dev/null @@ -1,18 +0,0 @@ -Feature: Project Issue Tracker - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" has issues enabled - And I visit project "Shop" page - - Scenario: I set the issue tracker to "GitLab" - When I visit edit project "Shop" page - And change the issue tracker to "GitLab" - And I save project - Then I the project should have "GitLab" as issue tracker - - Scenario: I set the issue tracker to "Redmine" - When I visit edit project "Shop" page - And change the issue tracker to "Redmine" - And I save project - Then I the project should have "Redmine" as issue tracker diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature index d9fbb875c28..ad1160e3343 100644 --- a/features/project/forked_merge_requests.feature +++ b/features/project/forked_merge_requests.feature @@ -38,3 +38,15 @@ Feature: Project Forked Merge Requests Given I visit project "Forked Shop" merge requests page And I click link "New Merge Request" Then the target repository should be the original repository + + @javascript + Scenario: I see the users in the target project for a new merge request + Given I logout + And I sign in as an admin + And I have a project forked off of "Shop" called "Forked Shop" + Then I visit project "Forked Shop" merge requests page + And I click link "New Merge Request" + And I fill out a "Merge Request On Forked Project" merge request + When I click "Assign to" dropdown" + Then I should see the target project ID in the input selector + And I should see the users from the target project ID diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature index 2c69a78a749..e316f519861 100644 --- a/features/project/issues/filter_labels.feature +++ b/features/project/issues/filter_labels.feature @@ -8,11 +8,7 @@ Feature: Project Issues Filter Labels And project "Shop" has issue "Feature1" with labels: "feature" Given I visit project "Shop" issues page - Scenario: I should see project issues - Then I should see "bug" in labels filter - And I should see "feature" in labels filter - And I should see "enhancement" in labels filter - + @javascript Scenario: I filter by one label Given I click link "bug" Then I should see "Bugfix1" in issues list diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 4db8551559b..bf84e2f8e87 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -25,6 +25,12 @@ Feature: Project Issues Given I click link "Release 0.4" Then I should see issue "Release 0.4" + @javascript + Scenario: I visit issue page + Given I add a user to project "Shop" + And I click "author" dropdown + Then I see current user as the first user + Scenario: I submit new unassigned issue Given I click link "New Issue" And I submit new issue "500 error on profile" @@ -42,6 +48,7 @@ Feature: Project Issues Given I visit issue page "Release 0.4" And I leave a comment like "XML attached" Then I should see comment "XML attached" + And I should see an error alert section within the comment form @javascript Scenario: I search issue @@ -127,35 +134,53 @@ Feature: Project Issues And I should see "Release 0.4" in issues And I should not see "Tweet control" in issues - Scenario: Issue description should render task checkboxes - Given project "Shop" has "Tasks-open" open issue with task markdown - When I visit issue page "Tasks-open" - Then I should see task checkboxes in the description + @javascript + Scenario: Issue notes should be editable with +1 + Given project "Shop" have "Release 0.4" open issue + When I visit issue page "Release 0.4" + And I leave a comment with a header containing "Comment with a header" + Then The comment with the header should not have an ID + And I edit the last comment with a +1 + Then I should see +1 in the description + + # Issue description preview @javascript - Scenario: Issue notes should not render task checkboxes - Given project "Shop" has "Tasks-open" open issue with task markdown - When I visit issue page "Tasks-open" - And I leave a comment with task markdown - Then I should not see task checkboxes in the comment + Scenario: I can't preview without text + Given I click link "New Issue" + And I haven't written any description text + Then The Markdown preview tab should say there is nothing to do - # Task status in issues list + @javascript + Scenario: I can preview with text + Given I click link "New Issue" + And I write a description like ":+1: Nice" + Then The Markdown preview tab should display rendered Markdown - Scenario: Issues list should display task status - Given project "Shop" has "Tasks-open" open issue with task markdown - When I visit project "Shop" issues page - Then I should see the task status for the Taskable + @javascript + Scenario: I preview an issue description + Given I click link "New Issue" + And I preview a description text like "Bug fixed :smile:" + Then I should see the Markdown preview + And I should not see the Markdown text field - # Toggling task items + @javascript + Scenario: I can edit after preview + Given I click link "New Issue" + And I preview a description text like "Bug fixed :smile:" + Then I should see the Markdown write tab @javascript - Scenario: Task checkboxes should be enabled for an open issue - Given project "Shop" has "Tasks-open" open issue with task markdown - When I visit issue page "Tasks-open" - Then Task checkboxes should be enabled + Scenario: I can preview when editing an existing issue + Given I click link "Release 0.4" + And I click link "Edit" for the issue + And I preview a description text like "Bug fixed :smile:" + Then I should see the Markdown write tab @javascript - Scenario: Task checkboxes should be disabled for a closed issue - Given project "Shop" has "Tasks-closed" closed issue with task markdown - When I visit issue page "Tasks-closed" - Then Task checkboxes should be disabled + Scenario: I can unsubscribe from issue + Given project "Shop" have "Release 0.4" open issue + When I visit issue page "Release 0.4" + Then I should see that I am subscribed + When I click button "Unsubscribe" + Then I should see that I am unsubscribed diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index d20358a7dc6..eb091c291e9 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -10,8 +10,8 @@ Feature: Project Merge Requests Then I should see "Bug NS-04" in merge requests And I should not see "Feature NS-03" in merge requests - Scenario: I should see closed merge requests - Given I click link "Closed" + Scenario: I should see rejected merge requests + Given I click link "Rejected" Then I should see "Feature NS-03" in merge requests And I should not see "Bug NS-04" in merge requests @@ -96,16 +96,6 @@ Feature: Project Merge Requests And I leave a comment with a header containing "Comment with a header" Then The comment with the header should not have an ID - Scenario: Merge request description should render task checkboxes - Given project "Shop" has "MR-task-open" open MR with task markdown - When I visit merge request page "MR-task-open" - Then I should see task checkboxes in the description - - Scenario: Merge request notes should not render task checkboxes - Given project "Shop" has "MR-task-open" open MR with task markdown - When I visit merge request page "MR-task-open" - Then I should not see task checkboxes in the comment - # Toggling inline comments @javascript @@ -166,24 +156,62 @@ Feature: Project Merge Requests And I click Side-by-side Diff tab Then I should see comments on the side-by-side diff page - # Task status in issues list + @javascript + Scenario: I view diffs on a merge request + Given project "Shop" have "Bug NS-05" open merge request with diffs inside + And I visit merge request page "Bug NS-05" + And I click on the Changes tab via Javascript + Then I should see the proper Inline and Side-by-side links + + # Description preview - Scenario: Merge requests list should display task status - Given project "Shop" has "MR-task-open" open MR with task markdown - When I visit project "Shop" merge requests page - Then I should see the task status for the Taskable + @javascript + Scenario: I can't preview without text + Given I visit merge request page "Bug NS-04" + And I click link "Edit" for the merge request + And I haven't written any description text + Then The Markdown preview tab should say there is nothing to do - # Toggling task items + @javascript + Scenario: I can preview with text + Given I visit merge request page "Bug NS-04" + And I click link "Edit" for the merge request + And I write a description like ":+1: Nice" + Then The Markdown preview tab should display rendered Markdown @javascript - Scenario: Task checkboxes should be enabled for an open merge request - Given project "Shop" has "MR-task-open" open MR with task markdown - When I visit merge request page "MR-task-open" - Then Task checkboxes should be enabled + Scenario: I preview a merge request description + Given I visit merge request page "Bug NS-04" + And I click link "Edit" for the merge request + And I preview a description text like "Bug fixed :smile:" + Then I should see the Markdown preview + And I should not see the Markdown text field @javascript - Scenario: Task checkboxes should be disabled for a closed merge request - Given project "Shop" has "MR-task-open" open MR with task markdown - And I visit merge request page "MR-task-open" - And I click link "Close" - Then Task checkboxes should be disabled + Scenario: I can edit after preview + Given I visit merge request page "Bug NS-04" + And I click link "Edit" for the merge request + And I preview a description text like "Bug fixed :smile:" + Then I should see the Markdown write tab + + @javascript + Scenario: I search merge request + Given I click link "All" + When I fill in merge request search with "Fe" + Then I should see "Feature NS-03" in merge requests + And I should not see "Bug NS-04" in merge requests + + @javascript + Scenario: I can unsubscribe from merge request + Given I visit merge request page "Bug NS-04" + Then I should see that I am subscribed + When I click button "Unsubscribe" + Then I should see that I am unsubscribed + + @javascript + Scenario: I can change the target branch + Given I visit merge request page "Bug NS-04" + And I click link "Edit" for the merge request + When I click the "Target branch" dropdown + And I select a new target branch + Then I should see new target branch changes diff --git a/features/project/project.feature b/features/project/project.feature index 7bb24e013a9..56ae5c78d01 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -5,6 +5,19 @@ Feature: Project And project "Shop" has push event And I visit project "Shop" page + Scenario: I edit the project avatar + Given I visit edit project "Shop" page + When I change the project avatar + And I should see new project avatar + And I should see the "Remove avatar" button + + Scenario: I remove the project avatar + Given I visit edit project "Shop" page + And I have an project avatar + When I remove my project avatar + Then I should see the default project avatar + And I should not see the "Remove avatar" button + @javascript Scenario: I should see project activity When I visit project "Shop" page @@ -42,3 +55,21 @@ Feature: Project Then I should see project "Forum" README And I visit project "Shop" page Then I should see project "Shop" README + + Scenario: I tag a project + When I visit edit project "Shop" page + Then I should see project settings + And I add project tags + And I save project + Then I should see project tags + + Scenario: I should not see "New Issue" or "New Merge Request" buttons + Given I disable issues and merge requests in project + When I visit project "Shop" page + Then I should not see "New Issue" button + And I should not see "New Merge Request" button + + Scenario: I should not see Project snippets + Given I disable snippets in project + When I visit project "Shop" page + Then I should not see "Snippets" button diff --git a/features/project/service.feature b/features/project/service.feature index ed9e03b428d..fdff640ec85 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -61,8 +61,26 @@ Feature: Project Services And I fill email on push settings Then I should see email on push service settings saved + Scenario: Activate Irker (IRC Gateway) service + When I visit project "Shop" services page + And I click Irker service link + And I fill Irker settings + Then I should see Irker service settings saved + Scenario: Activate Atlassian Bamboo CI service When I visit project "Shop" services page And I click Atlassian Bamboo CI service link And I fill Atlassian Bamboo CI settings Then I should see Atlassian Bamboo CI service settings saved + + Scenario: Activate jetBrains TeamCity CI service + When I visit project "Shop" services page + And I click jetBrains TeamCity CI service link + And I fill jetBrains TeamCity CI settings + Then I should see jetBrains TeamCity CI service settings saved + + Scenario: Activate Asana service + When I visit project "Shop" services page + And I click Asana service link + And I fill Asana settings + Then I should see Asana service settings saved diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index b7d70881d56..90b966dd645 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -35,6 +35,30 @@ Feature: Project Source Browse Files And I should see its new content @javascript + Scenario: I can create and commit file and specify new branch + Given I click on "new file" link in repo + And I edit code + And I fill the new file name + And I fill the commit message + And I fill the new branch name + And I click on "Commit Changes" + Then I am redirected to the new file on new branch + And I should see its new content + + @javascript @tricky + Scenario: I can create file in empty repo + Given I own an empty project + And I visit my empty project page + And I create bare repo + When I click on "add a file" link + And I edit code + And I fill the new file name + And I fill the commit message + And I click on "Commit Changes" + Then I am redirected to the new file + And I should see its new content + + @javascript Scenario: If I enter an illegal file name I see an error message Given I click on "new file" link in repo And I fill the new file name with an illegal name @@ -50,6 +74,16 @@ Feature: Project Source Browse Files And I click button "Edit" Then I can edit code + Scenario: If the file is binary the edit link is hidden + Given I visit a binary file in the repo + Then I cannot see the edit button + + Scenario: If I don't have edit permission the edit link is disabled + Given public project "Community" + And I visit project "Community" source page + And I click on ".gitignore" file in repo + Then The edit button is disabled + @javascript Scenario: I can edit and commit file Given I click on ".gitignore" file in repo @@ -60,6 +94,17 @@ Feature: Project Source Browse Files Then I am redirected to the ".gitignore" And I should see its new content + @javascript + Scenario: I can edit and commit file to new branch + Given I click on ".gitignore" file in repo + And I click button "Edit" + And I edit code + And I fill the commit message + And I fill the new branch name + And I click on "Commit Changes" + Then I am redirected to the ".gitignore" on new branch + And I should see its new content + @javascript @wip Scenario: If I don't change the content of the file I see an error message Given I click on ".gitignore" file in repo diff --git a/features/project/source/multiselect_blob.feature b/features/project/source/multiselect_blob.feature deleted file mode 100644 index 63b7cb77a93..00000000000 --- a/features/project/source/multiselect_blob.feature +++ /dev/null @@ -1,85 +0,0 @@ -Feature: Project Source Multiselect Blob - Background: - Given I sign in as a user - And I own project "Shop" - And I visit ".gitignore" file in repo - - @javascript - Scenario: I click line 1 in file - When I click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - - @javascript - Scenario: I shift-click line 1 in file - When I shift-click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - - @javascript - Scenario: I click line 1 then click line 2 in file - When I click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - Then I click line 2 in file - Then I should see "L2" as URI fragment - And I should see line 2 highlighted - - @javascript - Scenario: I click various line numbers to test multiselect - Then I click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - Then I shift-click line 2 in file - Then I should see "L1-2" as URI fragment - And I should see lines 1-2 highlighted - Then I shift-click line 3 in file - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I click line 3 in file - Then I should see "L3" as URI fragment - And I should see line 3 highlighted - Then I shift-click line 1 in file - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I shift-click line 5 in file - Then I should see "L1-5" as URI fragment - And I should see lines 1-5 highlighted - Then I shift-click line 4 in file - Then I should see "L1-4" as URI fragment - And I should see lines 1-4 highlighted - Then I click line 5 in file - Then I should see "L5" as URI fragment - And I should see line 5 highlighted - Then I shift-click line 3 in file - Then I should see "L3-5" as URI fragment - And I should see lines 3-5 highlighted - Then I shift-click line 1 in file - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I shift-click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - - @javascript - Scenario: I multiselect lines 1-5 and then go back and forward in history - When I click line 1 in file - And I shift-click line 3 in file - And I shift-click line 2 in file - And I shift-click line 5 in file - Then I should see "L1-5" as URI fragment - And I should see lines 1-5 highlighted - Then I go back in history - Then I should see "L1-2" as URI fragment - And I should see lines 1-2 highlighted - Then I go back in history - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I go back in history - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - Then I go forward in history - And I go forward in history - And I go forward in history - Then I should see "L1-5" as URI fragment - And I should see lines 1-5 highlighted diff --git a/features/project/star.feature b/features/project/star.feature index 3322f891805..a45f9c470ea 100644 --- a/features/project/star.feature +++ b/features/project/star.feature @@ -13,7 +13,7 @@ Feature: Project Star Given public project "Community" And I visit project "Community" page When I click on the star toggle button - Then The project has 0 stars + Then I redirected to sign in page @javascript Scenario: Signed in users can toggle star diff --git a/features/project/team_management.feature b/features/project/team_management.feature index 86ea6cd6e91..09a7df59df6 100644 --- a/features/project/team_management.feature +++ b/features/project/team_management.feature @@ -3,30 +3,36 @@ Feature: Project Team Management Given I sign in as a user And I own project "Shop" And gitlab user "Mike" - And gitlab user "Sam" - And "Sam" is "Shop" developer + And gitlab user "Dmitriy" + And "Dmitriy" is "Shop" developer And I visit project "Shop" team page Scenario: See all team members Then I should be able to see myself in team - And I should see "Sam" in team list + And I should see "Dmitriy" in team list @javascript Scenario: Add user to project - Given I click link "New Team Member" + Given I click link "Add members" And I select "Mike" as "Reporter" Then I should see "Mike" in team list as "Reporter" @javascript + Scenario: Invite user to project + Given I click link "Add members" + And I select "sjobs@apple.com" as "Reporter" + Then I should see "sjobs@apple.com" in team list as invited "Reporter" + + @javascript Scenario: Update user access - Given I should see "Sam" in team list as "Developer" - And I change "Sam" role to "Reporter" - And I should see "Sam" in team list as "Reporter" + Given I should see "Dmitriy" in team list as "Developer" + And I change "Dmitriy" role to "Reporter" + And I should see "Dmitriy" in team list as "Reporter" Scenario: Cancel team member - Given I click cancel link for "Sam" + Given I click cancel link for "Dmitriy" Then I visit project "Shop" team page - And I should not see "Sam" in team list + And I should not see "Dmitriy" in team list Scenario: Import team from another project Given I own project "Website" diff --git a/features/project/wiki.feature b/features/project/wiki.feature index 4a8c771ddac..2ebfa3c1660 100644 --- a/features/project/wiki.feature +++ b/features/project/wiki.feature @@ -62,3 +62,32 @@ Feature: Project Wiki And I browse to wiki page with images And I click on image link Then I should see the new wiki page form + + @javascript + Scenario: New Wiki page that has a path + Given I create a New page with paths + And I click on the "Pages" button + Then I should see non-escaped link in the pages list + + @javascript + Scenario: Creating an invalid new page + Given I create a New page with an invalid name + Then I should see an error message + + @javascript + Scenario: Edit Wiki page that has a path + Given I create a New page with paths + And I click on the "Pages" button + And I edit the Wiki page with a path + Then I should see a non-escaped path + And I should see the Editing page + And I change the content + Then I should see the updated content + + @javascript + Scenario: View the page history of a Wiki page that has a path + Given I create a New page with paths + And I click on the "Pages" button + And I view the page history of a Wiki page that has a path + Then I should see a non-escaped path + And I should see the page history diff --git a/features/search.feature b/features/search.feature index 54708c17575..1608e824671 100644 --- a/features/search.feature +++ b/features/search.feature @@ -13,15 +13,15 @@ Feature: Search And project has issues When I search for "Foo" And I click "Issues" link - Then I should see "Foo" link - And I should not see "Bar" link + Then I should see "Foo" link in the search results + And I should not see "Bar" link in the search results Scenario: I should see merge requests I am looking for And project has merge requests When I search for "Foo" When I click "Merge requests" link - Then I should see "Foo" link - And I should not see "Bar" link + Then I should see "Foo" link in the search results + And I should not see "Bar" link in the search results Scenario: I should see project code I am looking for When I click project "Shop" link @@ -33,14 +33,20 @@ Feature: Search When I click project "Shop" link And I search for "Foo" And I click "Issues" link - Then I should see "Foo" link - And I should not see "Bar" link + Then I should see "Foo" link in the search results + And I should not see "Bar" link in the search results Scenario: I should see project merge requests And project has merge requests When I click project "Shop" link And I search for "Foo" And I click "Merge requests" link - Then I should see "Foo" link - And I should not see "Bar" link + Then I should see "Foo" link in the search results + And I should not see "Bar" link in the search results + Scenario: I should see Wiki blobs + And project has Wiki content + When I click project "Shop" link + And I search for "Wiki content" + And I click "Wiki" link + Then I should see "test_wiki" link in the search results diff --git a/features/snippets/snippets.feature b/features/snippets/snippets.feature index 6e8019c326f..4f617b6bed8 100644 --- a/features/snippets/snippets.feature +++ b/features/snippets/snippets.feature @@ -25,4 +25,15 @@ Feature: Snippets Scenario: I destroy "Personal snippet one" Given I visit snippet page "Personal snippet one" And I click link "Destroy" - Then I should not see "Personal snippet one" in snippets
\ No newline at end of file + Then I should not see "Personal snippet one" in snippets + + Scenario: I create new internal snippet + Given I logout directly + And I sign in as an admin + Then I visit new snippet page + And I submit new internal snippet + Then I visit snippet page "Internal personal snippet one" + And I logout directly + Then I sign in as a user + Given I visit new snippet page + Then I visit snippet page "Internal personal snippet one" diff --git a/features/steps/admin/applications.rb b/features/steps/admin/applications.rb new file mode 100644 index 00000000000..7c12cb96921 --- /dev/null +++ b/features/steps/admin/applications.rb @@ -0,0 +1,55 @@ +class Spinach::Features::AdminApplications < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedAdmin + + step 'I click on new application button' do + click_on 'New Application' + end + + step 'I should see application form' do + expect(page).to have_content "New application" + end + + step 'I fill application form out and submit' do + fill_in :doorkeeper_application_name, with: 'test' + fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com' + click_on "Submit" + end + + step 'I see application' do + expect(page).to have_content "Application: test" + expect(page).to have_content "Application Id" + expect(page).to have_content "Secret" + end + + step 'I click edit' do + click_on "Edit" + end + + step 'I see edit application form' do + expect(page).to have_content "Edit application" + end + + step 'I change name of application and submit' do + expect(page).to have_content "Edit application" + fill_in :doorkeeper_application_name, with: 'test_changed' + click_on "Submit" + end + + step 'I see that application was changed' do + expect(page).to have_content "test_changed" + expect(page).to have_content "Application Id" + expect(page).to have_content "Secret" + end + + step 'I click to remove application' do + page.within '.oauth-applications' do + click_on "Destroy" + end + end + + step "I see that application is removed" do + expect(page.find(".oauth-applications")).not_to have_content "test_changed" + end +end diff --git a/features/steps/admin/broadcast_messages.rb b/features/steps/admin/broadcast_messages.rb index a35fa34a3a2..f6daf852977 100644 --- a/features/steps/admin/broadcast_messages.rb +++ b/features/steps/admin/broadcast_messages.rb @@ -8,7 +8,7 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps end step 'I should be all broadcast messages' do - page.should have_content "Migration to new server" + expect(page).to have_content "Migration to new server" end step 'submit form with new broadcast message' do @@ -18,11 +18,11 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps end step 'I should be redirected to admin messages page' do - current_path.should == admin_broadcast_messages_path + expect(current_path).to eq admin_broadcast_messages_path end step 'I should see newly created broadcast message' do - page.should have_content 'Application update from 4:00 CST to 5:00 CST' + expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST' end step 'submit form with new customized broadcast message' do @@ -35,7 +35,7 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps end step 'I should see a customized broadcast message' do - page.should have_content 'Application update from 4:00 CST to 5:00 CST' - page.should have_selector %(div[style="background-color:#f2dede;color:#b94a48"]) + expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST' + expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"]) end end diff --git a/features/steps/admin/deploy_keys.rb b/features/steps/admin/deploy_keys.rb new file mode 100644 index 00000000000..56787eeb6b3 --- /dev/null +++ b/features/steps/admin/deploy_keys.rb @@ -0,0 +1,46 @@ +class Spinach::Features::AdminDeployKeys < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedAdmin + + step 'there are public deploy keys in system' do + create(:deploy_key, public: true) + create(:another_deploy_key, public: true) + end + + step 'I should see all public deploy keys' do + DeployKey.are_public.each do |p| + expect(page).to have_content p.title + end + end + + step 'I visit admin deploy key page' do + visit admin_deploy_key_path(deploy_key) + end + + step 'I visit admin deploy keys page' do + visit admin_deploy_keys_path + end + + step 'I click \'New Deploy Key\'' do + click_link 'New Deploy Key' + end + + step 'I submit new deploy key' do + fill_in "deploy_key_title", with: "laptop" + fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop" + click_button "Create" + end + + step 'I should be on admin deploy keys page' do + expect(current_path).to eq admin_deploy_keys_path + end + + step 'I should see newly created deploy key' do + expect(page).to have_content(deploy_key.title) + end + + def deploy_key + @deploy_key ||= DeployKey.are_public.first + end +end diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb index d69a87cd07e..9cc74a97c3a 100644 --- a/features/steps/admin/groups.rb +++ b/features/steps/admin/groups.rb @@ -22,38 +22,38 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps end step 'submit form with new group info' do - fill_in 'group_name', with: 'gitlab' + fill_in 'group_path', with: 'gitlab' fill_in 'group_description', with: 'Group description' click_button "Create group" end step 'I should see newly created group' do - page.should have_content "Group: gitlab" - page.should have_content "Group description" + expect(page).to have_content "Group: gitlab" + expect(page).to have_content "Group description" end step 'I should be redirected to group page' do - current_path.should == admin_group_path(Group.last) + expect(current_path).to eq admin_group_path(Group.find_by(path: 'gitlab')) end When 'I select user "John Doe" from user list as "Reporter"' do select2(user_john.id, from: "#user_ids", multiple: true) - within "#new_team_member" do + page.within "#new_project_member" do select "Reporter", from: "access_level" end - click_button "Add users into group" + click_button "Add users to group" end step 'I should see "John Doe" in team list in every project as "Reporter"' do - within ".group-users-list" do - page.should have_content "John Doe" - page.should have_content "Reporter" + page.within ".group-users-list" do + expect(page).to have_content "John Doe" + expect(page).to have_content "Reporter" end end step 'I should be all groups' do Group.all.each do |group| - page.should have_content group.name + expect(page).to have_content group.name end end @@ -62,14 +62,14 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps end step 'I remove user "John Doe" from group' do - within "#user_#{user_john.id}" do + page.within "#user_#{user_john.id}" do click_link 'Remove user from group' end end step 'I should not see "John Doe" in team list' do - within ".group-users-list" do - page.should_not have_content "John Doe" + page.within ".group-users-list" do + expect(page).not_to have_content "John Doe" end end diff --git a/features/steps/admin/logs.rb b/features/steps/admin/logs.rb index 904e5468655..f9e49588c75 100644 --- a/features/steps/admin/logs.rb +++ b/features/steps/admin/logs.rb @@ -4,8 +4,8 @@ class Spinach::Features::AdminLogs < Spinach::FeatureSteps include SharedAdmin step 'I should see tabs with available logs' do - page.should have_content 'production.log' - page.should have_content 'githost.log' - page.should have_content 'application.log' + expect(page).to have_content 'production.log' + expect(page).to have_content 'githost.log' + expect(page).to have_content 'application.log' end end diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb index 2fd6385fe7b..655f1895279 100644 --- a/features/steps/admin/projects.rb +++ b/features/steps/admin/projects.rb @@ -5,7 +5,7 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps step 'I should see all projects' do Project.all.each do |p| - page.should have_content p.name_with_namespace + expect(page).to have_content p.name_with_namespace end end @@ -15,17 +15,17 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps step 'I should see project details' do project = Project.first - current_path.should == admin_project_path(project) - page.should have_content(project.name_with_namespace) - page.should have_content(project.creator.name) + expect(current_path).to eq admin_namespace_project_path(project.namespace, project) + expect(page).to have_content(project.name_with_namespace) + expect(page).to have_content(project.creator.name) end step 'I visit admin project page' do - visit admin_project_path(project) + visit admin_namespace_project_path(project.namespace, project) end step 'I transfer project to group \'Web\'' do - find(:xpath, "//input[@id='namespace_id']").set group.id + find(:xpath, "//input[@id='new_namespace_id']").set group.id click_button 'Transfer' end @@ -34,8 +34,8 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps end step 'I should see project transfered' do - page.should have_content 'Web / ' + project.name - page.should have_content 'Namespace: Web' + expect(page).to have_content 'Web / ' + project.name + expect(page).to have_content 'Namespace: Web' end def project diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb new file mode 100644 index 00000000000..147a4bd7486 --- /dev/null +++ b/features/steps/admin/settings.rb @@ -0,0 +1,58 @@ +class Spinach::Features::AdminSettings < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedAdmin + include Gitlab::CurrentSettings + + step 'I modify settings and save form' do + uncheck 'Gravatar enabled' + fill_in 'Home page url', with: 'https://about.gitlab.com/' + click_button 'Save' + end + + step 'I should see application settings saved' do + expect(current_application_settings.gravatar_enabled).to be_falsey + expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/" + expect(page).to have_content "Application settings saved successfully" + end + + step 'I click on "Service Templates"' do + click_link 'Service Templates' + end + + step 'I click on "Slack" service' do + click_link 'Slack' + end + + step 'I check all events and submit form' do + page.check('Active') + page.check('Push events') + page.check('Tag push events') + page.check('Comments') + page.check('Issues events') + page.check('Merge Request events') + click_on 'Save' + end + + step 'I fill out Slack settings' do + fill_in 'Webhook', with: 'http://localhost' + fill_in 'Username', with: 'test_user' + fill_in 'Channel', with: '#test_channel' + end + + step 'I should see service template settings saved' do + expect(page).to have_content 'Application settings saved successfully' + end + + step 'I should see all checkboxes checked' do + page.all('input[type=checkbox]').each do |checkbox| + expect(checkbox).to be_checked + end + end + + step 'I should see Slack settings saved' do + expect(find_field('Webhook').value).to eq 'http://localhost' + expect(find_field('Username').value).to eq 'test_user' + expect(find_field('Channel').value).to eq '#test_channel' + end +end diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb index 546c1bf2a12..34a3ed9f615 100644 --- a/features/steps/admin/users.rb +++ b/features/steps/admin/users.rb @@ -5,7 +5,7 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps step 'I should see all users' do User.all.each do |user| - page.should have_content user.name + expect(page).to have_content user.name end end @@ -23,13 +23,13 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'See username error message' do - within "#error_explanation" do - page.should have_content "Username" + page.within "#error_explanation" do + expect(page).to have_content "Username" end end step 'Not changed form action url' do - page.should have_selector %(form[action="/admin/users/#{@user.username}"]) + expect(page).to have_selector %(form[action="/admin/users/#{@user.username}"]) end step 'I submit modified user' do @@ -38,7 +38,7 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I see user attributes changed' do - page.should have_content 'Can create groups: Yes' + expect(page).to have_content 'Can create groups: Yes' end step 'click edit on my user' do @@ -53,7 +53,7 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I see the secondary email' do - page.should have_content "Secondary email: #{@user_with_secondary_email.emails.last.email}" + expect(page).to have_content "Secondary email: #{@user_with_secondary_email.emails.last.email}" end step 'I click remove secondary email' do @@ -61,7 +61,7 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I should not see secondary email anymore' do - page.should_not have_content "Secondary email:" + expect(page).not_to have_content "Secondary email:" end step 'user "Mike" with groups and projects' do @@ -79,7 +79,39 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I should see user "Mike" details' do - page.should have_content 'Account' - page.should have_content 'Personal projects limit' + expect(page).to have_content 'Account' + expect(page).to have_content 'Personal projects limit' + end + + step 'user "Pete" with ssh keys' do + user = create(:user, name: 'Pete') + create(:key, user: user, title: "ssh-rsa Key1", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1") + create(:key, user: user, title: "ssh-rsa Key2", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2") + end + + step 'click on user "Pete"' do + click_link 'Pete' + end + + step 'I should see key list' do + expect(page).to have_content 'ssh-rsa Key2' + expect(page).to have_content 'ssh-rsa Key1' + end + + step 'I click on the key title' do + click_link 'ssh-rsa Key2' + end + + step 'I should see key details' do + expect(page).to have_content 'ssh-rsa Key2' + expect(page).to have_content 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2' + end + + step 'I click on remove key' do + click_link 'Remove' + end + + step 'I should see the key removed' do + expect(page).not_to have_content 'ssh-rsa Key2' end end diff --git a/features/steps/dashboard/archived_projects.rb b/features/steps/dashboard/archived_projects.rb index 969baf92287..36e092f50c6 100644 --- a/features/steps/dashboard/archived_projects.rb +++ b/features/steps/dashboard/archived_projects.rb @@ -9,14 +9,14 @@ class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps end step 'I should see "Shop" project link' do - page.should have_link "Shop" + expect(page).to have_link "Shop" end step 'I should not see "Forum" project link' do - page.should_not have_link "Forum" + expect(page).not_to have_link "Forum" end step 'I should see "Forum" project link' do - page.should have_link "Forum" + expect(page).to have_link "Forum" end end diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 1826ead1d51..945bf35ff27 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -4,16 +4,16 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps include SharedProject step 'I should see "New Project" link' do - page.should have_link "New project" + expect(page).to have_link "New project" end step 'I should see "Shop" project link' do - page.should have_link "Shop" + expect(page).to have_link "Shop" end step 'I should see last push widget' do - page.should have_content "You pushed to fix" - page.should have_link "Create Merge Request" + expect(page).to have_content "You pushed to fix" + expect(page).to have_link "Create Merge Request" end step 'I click "Create Merge Request" link' do @@ -21,10 +21,10 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'I see prefilled new Merge Request page' do - current_path.should == new_project_merge_request_path(@project) - find("#merge_request_target_project_id").value.should == @project.id.to_s - find("#merge_request_source_branch").value.should == "fix" - find("#merge_request_target_branch").value.should == "master" + expect(current_path).to eq new_namespace_project_merge_request_path(@project.namespace, @project) + expect(find("#merge_request_target_project_id").value).to eq @project.id.to_s + expect(find("input#merge_request_source_branch").value).to eq "fix" + expect(find("input#merge_request_target_branch").value).to eq "master" end step 'user with name "John Doe" joined project "Shop"' do @@ -37,8 +37,8 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps ) end - step 'I should see "John Doe joined project at Shop" event' do - page.should have_content "John Doe joined project at #{project.name_with_namespace}" + step 'I should see "John Doe joined project Shop" event' do + expect(page).to have_content "John Doe joined project #{project.name_with_namespace}" end step 'user with name "John Doe" left project "Shop"' do @@ -50,8 +50,8 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps ) end - step 'I should see "John Doe left project at Shop" event' do - page.should have_content "John Doe left project at #{project.name_with_namespace}" + step 'I should see "John Doe left project Shop" event' do + expect(page).to have_content "John Doe left project #{project.name_with_namespace}" end step 'I have group with projects' do @@ -64,13 +64,13 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps step 'I should see projects list' do @user.authorized_projects.all.each do |project| - page.should have_link project.name_with_namespace + expect(page).to have_link project.name_with_namespace end end step 'I should see groups list' do Group.all.each do |group| - page.should have_link group.name + expect(page).to have_link group.name end end @@ -80,6 +80,6 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'I should see 1 project at group list' do - find('span.last_activity/span').should have_content('1') + expect(find('span.last_activity/span')).to have_content('1') end end diff --git a/features/steps/dashboard/event_filters.rb b/features/steps/dashboard/event_filters.rb index 3da3d62d0c0..834afa439a0 100644 --- a/features/steps/dashboard/event_filters.rb +++ b/features/steps/dashboard/event_filters.rb @@ -4,27 +4,27 @@ class Spinach::Features::EventFilters < Spinach::FeatureSteps include SharedProject step 'I should see push event' do - page.should have_selector('span.pushed') + expect(page).to have_selector('span.pushed') end step 'I should not see push event' do - page.should_not have_selector('span.pushed') + expect(page).not_to have_selector('span.pushed') end step 'I should see new member event' do - page.should have_selector('span.joined') + expect(page).to have_selector('span.joined') end step 'I should not see new member event' do - page.should_not have_selector('span.joined') + expect(page).not_to have_selector('span.joined') end step 'I should see merge request event' do - page.should have_selector('span.accepted') + expect(page).to have_selector('span.accepted') end step 'I should not see merge request event' do - page.should_not have_selector('span.accepted') + expect(page).not_to have_selector('span.accepted') end step 'this project has push event' do diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb new file mode 100644 index 00000000000..0c6a0ae3725 --- /dev/null +++ b/features/steps/dashboard/group.rb @@ -0,0 +1,67 @@ +class Spinach::Features::DashboardGroup < Spinach::FeatureSteps + include SharedAuthentication + include SharedGroup + include SharedPaths + include SharedUser + + # Leave + + step 'I click on the "Leave" button for group "Owned"' do + find(:css, 'li', text: "Owner").find(:css, 'i.fa.fa-sign-out').click + # poltergeist always confirms popups. + end + + step 'I click on the "Leave" button for group "Guest"' do + find(:css, 'li', text: "Guest").find(:css, 'i.fa.fa-sign-out').click + # poltergeist always confirms popups. + end + + step 'I should not see the "Leave" button for group "Owned"' do + expect(find(:css, 'li', text: "Owner")).not_to have_selector(:css, 'i.fa.fa-sign-out') + # poltergeist always confirms popups. + end + + step 'I should not see the "Leave" button for groupr "Guest"' do + expect(find(:css, 'li', text: "Guest")).not_to have_selector(:css, 'i.fa.fa-sign-out') + # poltergeist always confirms popups. + end + + step 'I should see group "Owned" in group list' do + expect(page).to have_content("Owned") + end + + step 'I should not see group "Owned" in group list' do + expect(page).not_to have_content("Owned") + end + + step 'I should see group "Guest" in group list' do + expect(page).to have_content("Guest") + end + + step 'I should not see group "Guest" in group list' do + expect(page).not_to have_content("Guest") + end + + step 'I click new group link' do + click_link "New Group" + end + + step 'submit form with new group "Samurai" info' do + fill_in 'group_path', with: 'Samurai' + fill_in 'group_description', with: 'Tokugawa Shogunate' + click_button "Create group" + end + + step 'I should be redirected to group "Samurai" page' do + expect(current_path).to eq group_path(Group.find_by(name: 'Samurai')) + end + + step 'I should see newly created group "Samurai"' do + expect(page).to have_content "Samurai" + expect(page).to have_content "Tokugawa Shogunate" + end + + step 'I should see the "Can not leave message"' do + expect(page).to have_content "You can not leave Owned group because you're the last owner" + end +end diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb index ef433c57c6e..86ab31a58ab 100644 --- a/features/steps/dashboard/help.rb +++ b/features/steps/dashboard/help.rb @@ -12,7 +12,7 @@ class Spinach::Features::DashboardHelp < Spinach::FeatureSteps end step 'I should see "Rake Tasks" page markdown rendered' do - page.should have_content "Gather information about GitLab and the system it runs on" + expect(page).to have_content "Gather information about GitLab and the system it runs on" end step 'Header "Rebuild project satellites" should have correct ids and links' do diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index 2a5850d091b..cbe54e2dc79 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -1,6 +1,7 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps include SharedAuthentication include SharedPaths + include Select2Helper step 'I should see issues assigned to me' do should_see(assigned_issue) @@ -35,23 +36,21 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps end step 'I click "Authored by me" link' do - within ".scope-filter" do - click_link 'Created by me' - end + select2(current_user.id, from: "#author_id") + select2(nil, from: "#assignee_id") end step 'I click "All" link' do - within ".scope-filter" do - click_link "Everyone's" - end + select2(nil, from: "#author_id") + select2(nil, from: "#assignee_id") end def should_see(issue) - page.should have_content(issue.title[0..10]) + expect(page).to have_content(issue.title[0..10]) end def should_not_see(issue) - page.should_not have_content(issue.title[0..10]) + expect(page).not_to have_content(issue.title[0..10]) end def assigned_issue diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb index 75e53173d3f..cec8d06adee 100644 --- a/features/steps/dashboard/merge_requests.rb +++ b/features/steps/dashboard/merge_requests.rb @@ -1,6 +1,7 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps include SharedAuthentication include SharedPaths + include Select2Helper step 'I should see merge requests assigned to me' do should_see(assigned_merge_request) @@ -39,23 +40,21 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps end step 'I click "Authored by me" link' do - within ".scope-filter" do - click_link 'Created by me' - end + select2(current_user.id, from: "#author_id") + select2(nil, from: "#assignee_id") end step 'I click "All" link' do - within ".scope-filter" do - click_link "Everyone's" - end + select2(nil, from: "#author_id") + select2(nil, from: "#assignee_id") end def should_see(merge_request) - page.should have_content(merge_request.title[0..10]) + expect(page).to have_content(merge_request.title[0..10]) end def should_not_see(merge_request) - page.should_not have_content(merge_request.title[0..10]) + expect(page).not_to have_content(merge_request.title[0..10]) end def assigned_merge_request diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb new file mode 100644 index 00000000000..d4440c1fb4d --- /dev/null +++ b/features/steps/dashboard/new_project.rb @@ -0,0 +1,29 @@ +class Spinach::Features::NewProject < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + + step 'I click "New project" link' do + page.within('.content') do + click_link "New project" + end + end + + step 'I see "New project" page' do + expect(page).to have_content('Project path') + end + + step 'I click on "Import project from GitHub"' do + first('.how_to_import_link').click + end + + step 'I see instructions on how to import from GitHub' do + github_modal = first('.modal-body') + expect(github_modal).to be_visible + expect(github_modal).to have_content "To enable importing projects from GitHub" + + page.all('.modal-body').each do |element| + expect(element).not_to be_visible unless element == github_modal + end + end +end diff --git a/features/steps/dashboard/projects.rb b/features/steps/dashboard/projects.rb deleted file mode 100644 index 2a348163060..00000000000 --- a/features/steps/dashboard/projects.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Spinach::Features::DashboardProjects < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - - step 'I should see projects list' do - @user.authorized_projects.all.each do |project| - page.should have_link project.name_with_namespace - end - end -end diff --git a/features/steps/dashboard/starred_projects.rb b/features/steps/dashboard/starred_projects.rb new file mode 100644 index 00000000000..59c73fe63f2 --- /dev/null +++ b/features/steps/dashboard/starred_projects.rb @@ -0,0 +1,15 @@ +class Spinach::Features::DashboardStarredProjects < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + + step 'I starred project "Community"' do + current_user.toggle_star(Project.find_by(name: 'Community')) + end + + step 'I should not see project "Shop"' do + page.within 'aside' do + expect(page).not_to have_content('Shop') + end + end +end diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb index ccbf6cda07e..89b82293ef2 100644 --- a/features/steps/explore/groups.rb +++ b/features/steps/explore/groups.rb @@ -35,23 +35,23 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps end step 'I visit group "TestGroup" members page' do - visit members_group_path(Group.find_by(name: "TestGroup")) + visit group_group_members_path(Group.find_by(name: "TestGroup")) end step 'I should not see project "Enterprise" items' do - page.should_not have_content "Enterprise" + expect(page).not_to have_content "Enterprise" end step 'I should see project "Internal" items' do - page.should have_content "Internal" + expect(page).to have_content "Internal" end step 'I should not see project "Internal" items' do - page.should_not have_content "Internal" + expect(page).not_to have_content "Internal" end step 'I should see project "Community" items' do - page.should have_content "Community" + expect(page).to have_content "Community" end step 'I change filter to Everyone\'s' do @@ -59,11 +59,11 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps end step 'I should see group member "John Doe"' do - page.should have_content "John Doe" + expect(page).to have_content "John Doe" end step 'I should not see member roles' do - body.should_not match(%r{owner|developer|reporter|guest}i) + expect(body).not_to match(%r{owner|developer|reporter|guest}i) end protected diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb index 8172f7922cc..49c2f6a1253 100644 --- a/features/steps/explore/projects.rb +++ b/features/steps/explore/projects.rb @@ -4,56 +4,56 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps include SharedProject step 'I should see project "Empty Public Project"' do - page.should have_content "Empty Public Project" + expect(page).to have_content "Empty Public Project" end step 'I should see public project details' do - page.should have_content '32 branches' - page.should have_content '16 tags' + expect(page).to have_content '32 branches' + expect(page).to have_content '16 tags' end step 'I should see project readme' do - page.should have_content 'README.md' + expect(page).to have_content 'README.md' end step 'I should see empty public project details' do - page.should have_content 'Git global setup' + expect(page).to have_content 'Git global setup' end step 'I should see empty public project details with http clone info' do project = Project.find_by(name: 'Empty Public Project') - all(:css, '.git-empty .clone').each do |element| - element.text.should include(project.http_url_to_repo) + page.all(:css, '.git-empty .clone').each do |element| + expect(element.text).to include(project.http_url_to_repo) end end step 'I should see empty public project details with ssh clone info' do project = Project.find_by(name: 'Empty Public Project') - all(:css, '.git-empty .clone').each do |element| - element.text.should include(project.url_to_repo) + page.all(:css, '.git-empty .clone').each do |element| + expect(element.text).to include(project.url_to_repo) end end step 'I should see project "Community" home page' do - within '.navbar-gitlab .title' do - page.should have_content 'Community' + page.within '.navbar-gitlab .title' do + expect(page).to have_content 'Community' end end step 'I should see project "Internal" home page' do - within '.navbar-gitlab .title' do - page.should have_content 'Internal' + page.within '.navbar-gitlab .title' do + expect(page).to have_content 'Internal' end end step 'I should see an http link to the repository' do project = Project.find_by(name: 'Community') - page.should have_field('project_clone', with: project.http_url_to_repo) + expect(page).to have_field('project_clone', with: project.http_url_to_repo) end step 'I should see an ssh link to the repository' do project = Project.find_by(name: 'Community') - page.should have_field('project_clone', with: project.url_to_repo) + expect(page).to have_field('project_clone', with: project.url_to_repo) end step 'I visit "Community" issues page' do @@ -65,14 +65,14 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps title: "New feature", project: public_project ) - visit project_issues_path(public_project) + visit namespace_project_issues_path(public_project.namespace, public_project) end step 'I should see list of issues for "Community" project' do - page.should have_content "Bug" - page.should have_content public_project.name - page.should have_content "New feature" + expect(page).to have_content "Bug" + expect(page).to have_content public_project.name + expect(page).to have_content "New feature" end step 'I visit "Internal" issues page' do @@ -84,18 +84,18 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps title: "New internal feature", project: internal_project ) - visit project_issues_path(internal_project) + visit namespace_project_issues_path(internal_project.namespace, internal_project) end step 'I should see list of issues for "Internal" project' do - page.should have_content "Internal Bug" - page.should have_content internal_project.name - page.should have_content "New internal feature" + expect(page).to have_content "Internal Bug" + expect(page).to have_content internal_project.name + expect(page).to have_content "New internal feature" end step 'I visit "Community" merge requests page' do - visit project_merge_requests_path(public_project) + visit namespace_project_merge_requests_path(public_project.namespace, public_project) end step 'project "Community" has "Bug fix" open merge request' do @@ -107,12 +107,12 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps end step 'I should see list of merge requests for "Community" project' do - page.should have_content public_project.name - page.should have_content public_merge_request.source_project.name + expect(page).to have_content public_project.name + expect(page).to have_content public_merge_request.source_project.name end step 'I visit "Internal" merge requests page' do - visit project_merge_requests_path(internal_project) + visit namespace_project_merge_requests_path(internal_project.namespace, internal_project) end step 'project "Internal" has "Feature implemented" open merge request' do @@ -124,8 +124,8 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps end step 'I should see list of merge requests for "Internal" project' do - page.should have_content internal_project.name - page.should have_content internal_merge_request.source_project.name + expect(page).to have_content internal_project.name + expect(page).to have_content internal_merge_request.source_project.name end def internal_project diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 616a297db99..6221163ac54 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -5,56 +5,99 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include SharedUser include Select2Helper + step 'gitlab user "Mike"' do + create(:user, name: "Mike") + end + + step 'I click link "Add members"' do + find(:css, 'button.btn-new').click + end + + step 'I select "Mike" as "Reporter"' do + user = User.find_by(name: "Mike") + + page.within ".users-group-form" do + select2(user.id, from: "#user_ids", multiple: true) + select "Reporter", from: "access_level" + end + + click_button "Add users to group" + end + + step 'I should see "Mike" in team list as "Reporter"' do + page.within '.well-list' do + expect(page).to have_content('Mike') + expect(page).to have_content('Reporter') + end + end + + step 'I select "sjobs@apple.com" as "Reporter"' do + page.within ".users-group-form" do + select2("sjobs@apple.com", from: "#user_ids", multiple: true) + select "Reporter", from: "access_level" + end + + click_button "Add users to group" + end + + step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do + page.within '.well-list' do + expect(page).to have_content('sjobs@apple.com') + expect(page).to have_content('invited') + expect(page).to have_content('Reporter') + end + end + step 'I should see group "Owned" projects list' do Group.find_by(name: "Owned").projects.each do |project| - page.should have_link project.name + expect(page).to have_link project.name end end step 'I should see projects activity feed' do - page.should have_content 'closed issue' + expect(page).to have_content 'closed issue' end step 'I should see issues from group "Owned" assigned to me' do assigned_to_me(:issues).each do |issue| - page.should have_content issue.title + expect(page).to have_content issue.title end end step 'I should see merge requests from group "Owned" assigned to me' do assigned_to_me(:merge_requests).each do |issue| - page.should have_content issue.title[0..80] + expect(page).to have_content issue.title[0..80] end end step 'I select user "Mary Jane" from list with role "Reporter"' do user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane") - click_link 'Add members' - within ".users-group-form" do + click_button 'Add members' + page.within ".users-group-form" do select2(user.id, from: "#user_ids", multiple: true) select "Reporter", from: "access_level" end - click_button "Add users into group" + click_button "Add users to group" end step 'I should see user "John Doe" in team list' do projects_with_access = find(".panel .well-list") - projects_with_access.should have_content("John Doe") + expect(projects_with_access).to have_content("John Doe") end step 'I should not see user "John Doe" in team list' do projects_with_access = find(".panel .well-list") - projects_with_access.should_not have_content("John Doe") + expect(projects_with_access).not_to have_content("John Doe") end step 'I should see user "Mary Jane" in team list' do projects_with_access = find(".panel .well-list") - projects_with_access.should have_content("Mary Jane") + expect(projects_with_access).to have_content("Mary Jane") end step 'I should not see user "Mary Jane" in team list' do projects_with_access = find(".panel .well-list") - projects_with_access.should_not have_content("Mary Jane") + expect(projects_with_access).not_to have_content("Mary Jane") end step 'project from group "Owned" has issues assigned to me' do @@ -72,34 +115,15 @@ class Spinach::Features::Groups < Spinach::FeatureSteps author: current_user end - When 'I click new group link' do - click_link "New group" - end - - step 'submit form with new group "Samurai" info' do - fill_in 'group_name', with: 'Samurai' - fill_in 'group_description', with: 'Tokugawa Shogunate' - click_button "Create group" - end - - step 'I should be redirected to group "Samurai" page' do - current_path.should == group_path(Group.last) - end - - step 'I should see newly created group "Samurai"' do - page.should have_content "Samurai" - page.should have_content "Tokugawa Shogunate" - page.should have_content "Currently you are only seeing events from the" - end - step 'I change group "Owned" name to "new-name"' do fill_in 'group_name', with: 'new-name' + fill_in 'group_path', with: 'new-name' click_button "Save group" end step 'I should see new group "Owned" name' do - within ".navbar-gitlab" do - page.should have_content "group: new-name" + page.within ".navbar-gitlab" do + expect(page).to have_content "new-name" end end @@ -110,12 +134,12 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see new group "Owned" avatar' do - Group.find_by(name: "Owned").avatar.should be_instance_of AttachmentUploader - Group.find_by(name: "Owned").avatar.url.should == "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/gitlab_logo.png" + expect(Group.find_by(name: "Owned").avatar).to be_instance_of AvatarUploader + expect(Group.find_by(name: "Owned").avatar.url).to eq "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/gitlab_logo.png" end step 'I should see the "Remove avatar" button' do - page.should have_link("Remove avatar") + expect(page).to have_link("Remove avatar") end step 'I have group "Owned" avatar' do @@ -130,11 +154,11 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should not see group "Owned" avatar' do - Group.find_by(name: "Owned").avatar?.should be_false + expect(Group.find_by(name: "Owned").avatar?).to eq false end step 'I should not see the "Remove avatar" button' do - page.should_not have_link("Remove avatar") + expect(page).not_to have_link("Remove avatar") end step 'I click on the "Remove User From Group" button for "John Doe"' do @@ -148,17 +172,17 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should not see the "Remove User From Group" button for "John Doe"' do - find(:css, 'li', text: "John Doe").should_not have_selector(:css, 'a.btn-remove') + expect(find(:css, 'li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove') # poltergeist always confirms popups. end step 'I should not see the "Remove User From Group" button for "Mary Jane"' do - find(:css, 'li', text: "Mary Jane").should_not have_selector(:css, 'a.btn-remove') + expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove') # poltergeist always confirms popups. end step 'I search for \'Mary\' member' do - within '.member-search-form' do + page.within '.member-search-form' do fill_in 'search', with: 'Mary' click_button 'Search' end @@ -169,7 +193,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see group milestones index page has no milestones' do - page.should have_content('No milestones to show') + expect(page).to have_content('No milestones to show') end step 'Group has projects with milestones' do @@ -177,10 +201,10 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see group milestones index page with milestones' do - page.should have_content('Version 7.2') - page.should have_content('GL-113') - page.should have_link('2 Issues', href: group_milestone_path("owned", "version-7-2", title: "Version 7.2")) - page.should have_link('3 Merge Requests', href: group_milestone_path("owned", "gl-113", title: "GL-113")) + expect(page).to have_content('Version 7.2') + expect(page).to have_content('GL-113') + expect(page).to have_link('2 Issues', href: issues_group_path("owned", milestone_title: "Version 7.2")) + expect(page).to have_link('3 Merge Requests', href: merge_requests_group_path("owned", milestone_title: "GL-113")) end step 'I click on one group milestone' do @@ -188,15 +212,14 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see group milestone with descriptions and expiry date' do - page.should have_content('Lorem Ipsum is simply dummy text of the printing and typesetting industry') - page.should have_content('expires at Aug 20, 2114') + expect(page).to have_content('expires at Aug 20, 2114') end step 'I should see group milestone with all issues and MRs assigned to that milestone' do - page.should have_content('Milestone GL-113') - page.should have_content('Progress: 0 closed – 4 open') - page.should have_link(@issue1.title, href: project_issue_path(@project1, @issue1)) - page.should have_link(@mr3.title, href: project_merge_request_path(@project3, @mr3)) + expect(page).to have_content('Milestone GL-113') + expect(page).to have_content('Progress: 0 closed – 4 open') + expect(page).to have_link(@issue1.title, href: namespace_project_issue_path(@project1.namespace, @project1, @issue1)) + expect(page).to have_link(@mr3.title, href: namespace_project_merge_request_path(@project3.namespace, @project3, @mr3)) end protected diff --git a/features/steps/invites.rb b/features/steps/invites.rb new file mode 100644 index 00000000000..d051cc3edc8 --- /dev/null +++ b/features/steps/invites.rb @@ -0,0 +1,80 @@ +class Spinach::Features::Invites < Spinach::FeatureSteps + include SharedAuthentication + include SharedUser + include SharedGroup + + step '"John Doe" has invited "user@example.com" to group "Owned"' do + user = User.find_by(name: "John Doe") + group = Group.find_by(name: "Owned") + group.add_user("user@example.com", Gitlab::Access::DEVELOPER, user) + end + + step 'I visit the invitation page' do + group = Group.find_by(name: "Owned") + invite = group.group_members.invite.last + invite.generate_invite_token! + @raw_invite_token = invite.raw_invite_token + visit invite_path(@raw_invite_token) + end + + step 'I should be redirected to the sign in page' do + expect(current_path).to eq(new_user_session_path) + end + + step 'I should see a notice telling me to sign in' do + expect(page).to have_content "To accept this invitation, sign in" + end + + step 'I should be redirected to the invitation page' do + expect(current_path).to eq(invite_path(@raw_invite_token)) + end + + step 'I should see the invitation details' do + expect(page).to have_content("You have been invited by John Doe to join group Owned as Developer.") + end + + step "I should see a message telling me I'm already a member" do + expect(page).to have_content("However, you are already a member of this group.") + end + + step 'I should see an "Accept invitation" button' do + expect(page).to have_link("Accept invitation") + end + + step 'I should see a "Decline" button' do + expect(page).to have_link("Decline") + end + + step 'I click the "Accept invitation" button' do + page.click_link "Accept invitation" + end + + step 'I should be redirected to the group page' do + group = Group.find_by(name: "Owned") + expect(current_path).to eq(group_path(group)) + end + + step 'I should see a notice telling me I have access' do + expect(page).to have_content("You have been granted Developer access to group Owned.") + end + + step 'I click the "Decline" button' do + page.click_link "Decline" + end + + step 'I should be redirected to the dashboard' do + expect(current_path).to eq(dashboard_path) + end + + step 'I should see a notice telling me I have declined' do + expect(page).to have_content("You have declined the invitation to join group Owned.") + end + + step "I visit the invitation's decline page" do + group = Group.find_by(name: "Owned") + invite = group.group_members.invite.last + invite.generate_invite_token! + @raw_invite_token = invite.raw_invite_token + visit decline_invite_path(@raw_invite_token) + end +end diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb index 8595ee876a4..79e3b55f6e1 100644 --- a/features/steps/profile/active_tab.rb +++ b/features/steps/profile/active_tab.rb @@ -15,8 +15,8 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps ensure_active_main_tab('SSH Keys') end - step 'the active main tab should be Design' do - ensure_active_main_tab('Design') + step 'the active main tab should be Preferences' do + ensure_active_main_tab('Preferences') end step 'the active main tab should be History' do diff --git a/features/steps/profile/emails.rb b/features/steps/profile/emails.rb index 2b6ac37d866..10ebe705365 100644 --- a/features/steps/profile/emails.rb +++ b/features/steps/profile/emails.rb @@ -6,9 +6,9 @@ class Spinach::Features::ProfileEmails < Spinach::FeatureSteps end step 'I should see my emails' do - page.should have_content(@user.email) + expect(page).to have_content(@user.email) @user.emails.each do |email| - page.should have_content(email.email) + expect(page).to have_content(email.email) end end @@ -19,14 +19,14 @@ class Spinach::Features::ProfileEmails < Spinach::FeatureSteps step 'I should see new email "my@email.com"' do email = @user.emails.find_by(email: "my@email.com") - email.should_not be_nil - page.should have_content("my@email.com") + expect(email).not_to be_nil + expect(page).to have_content("my@email.com") end step 'I should not see email "my@email.com"' do email = @user.emails.find_by(email: "my@email.com") - email.should be_nil - page.should_not have_content("my@email.com") + expect(email).to be_nil + expect(page).not_to have_content("my@email.com") end step 'I click link "Remove" for "my@email.com"' do @@ -43,6 +43,6 @@ class Spinach::Features::ProfileEmails < Spinach::FeatureSteps step 'I should not have @user.email added' do email = @user.emails.find_by(email: @user.email) - email.should be_nil + expect(email).to be_nil end end diff --git a/features/steps/profile/group.rb b/features/steps/profile/group.rb deleted file mode 100644 index 0a10e04e219..00000000000 --- a/features/steps/profile/group.rb +++ /dev/null @@ -1,44 +0,0 @@ -class Spinach::Features::ProfileGroup < Spinach::FeatureSteps - include SharedAuthentication - include SharedGroup - include SharedPaths - include SharedUser - - # Leave - - step 'I click on the "Leave" button for group "Owned"' do - find(:css, 'li', text: "Owner").find(:css, 'i.fa.fa-sign-out').click - # poltergeist always confirms popups. - end - - step 'I click on the "Leave" button for group "Guest"' do - find(:css, 'li', text: "Guest").find(:css, 'i.fa.fa-sign-out').click - # poltergeist always confirms popups. - end - - step 'I should not see the "Leave" button for group "Owned"' do - find(:css, 'li', text: "Owner").should_not have_selector(:css, 'i.fa.fa-sign-out') - # poltergeist always confirms popups. - end - - step 'I should not see the "Leave" button for groupr "Guest"' do - find(:css, 'li', text: "Guest").should_not have_selector(:css, 'i.fa.fa-sign-out') - # poltergeist always confirms popups. - end - - step 'I should see group "Owned" in group list' do - page.should have_content("Owned") - end - - step 'I should not see group "Owned" in group list' do - page.should_not have_content("Owned") - end - - step 'I should see group "Guest" in group list' do - page.should have_content("Guest") - end - - step 'I should not see group "Guest" in group list' do - page.should_not have_content("Guest") - end -end diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb index df96dddd06e..447ea6d9d10 100644 --- a/features/steps/profile/notifications.rb +++ b/features/steps/profile/notifications.rb @@ -7,6 +7,6 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps end step 'I should see global notifications settings' do - page.should have_content "Notifications settings" + expect(page).to have_content "Notifications" end end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 6d747b65bae..3f19bed8a0b 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -3,23 +3,27 @@ class Spinach::Features::Profile < Spinach::FeatureSteps include SharedPaths step 'I should see my profile info' do - page.should have_content "Profile settings" + expect(page).to have_content "This information will appear on your profile" end step 'I change my profile info' do - fill_in "user_skype", with: "testskype" - fill_in "user_linkedin", with: "testlinkedin" - fill_in "user_twitter", with: "testtwitter" - fill_in "user_website_url", with: "testurl" - click_button "Save changes" + fill_in 'user_skype', with: 'testskype' + fill_in 'user_linkedin', with: 'testlinkedin' + fill_in 'user_twitter', with: 'testtwitter' + fill_in 'user_website_url', with: 'testurl' + fill_in 'user_location', with: 'Ukraine' + fill_in 'user_bio', with: 'I <3 GitLab' + click_button 'Save changes' @user.reload end step 'I should see new profile info' do - @user.skype.should == 'testskype' - @user.linkedin.should == 'testlinkedin' - @user.twitter.should == 'testtwitter' - @user.website_url.should == 'testurl' + expect(@user.skype).to eq 'testskype' + expect(@user.linkedin).to eq 'testlinkedin' + expect(@user.twitter).to eq 'testtwitter' + expect(@user.website_url).to eq 'testurl' + expect(@user.bio).to eq 'I <3 GitLab' + expect(find('#user_location').value).to eq 'Ukraine' end step 'I change my avatar' do @@ -29,12 +33,12 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see new avatar' do - @user.avatar.should be_instance_of AttachmentUploader - @user.avatar.url.should == "/uploads/user/avatar/#{ @user.id }/gitlab_logo.png" + expect(@user.avatar).to be_instance_of AvatarUploader + expect(@user.avatar.url).to eq "/uploads/user/avatar/#{ @user.id }/gitlab_logo.png" end step 'I should see the "Remove avatar" button' do - page.should have_link("Remove avatar") + expect(page).to have_link("Remove avatar") end step 'I have an avatar' do @@ -49,91 +53,61 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see my gravatar' do - @user.avatar?.should be_false + expect(@user.avatar?).to eq false end step 'I should not see the "Remove avatar" button' do - page.should_not have_link("Remove avatar") + expect(page).not_to have_link("Remove avatar") end step 'I try change my password w/o old one' do - within '.update-password' do - fill_in "user_password_profile", with: "22233344" + page.within '.update-password' do + fill_in "user_password", with: "22233344" fill_in "user_password_confirmation", with: "22233344" click_button "Save" end end - step 'I try to set a weak password' do - within '.update-password' do - fill_in "user_password_profile", with: "22233344" - end - end - - step 'I try to set a short password' do - within '.update-password' do - fill_in "user_password_profile", with: "short" - end - end - - step 'I try to set a strong password' do - within '.update-password' do - fill_in "user_password_profile", with: "Itulvo9z8uud%$" - end - end - step 'I change my password' do - within '.update-password' do + page.within '.update-password' do fill_in "user_current_password", with: "12345678" - fill_in "user_password_profile", with: "22233344" + fill_in "user_password", with: "22233344" fill_in "user_password_confirmation", with: "22233344" click_button "Save" end end step 'I unsuccessfully change my password' do - within '.update-password' do + page.within '.update-password' do fill_in "user_current_password", with: "12345678" - fill_in "user_password_profile", with: "password" + fill_in "user_password", with: "password" fill_in "user_password_confirmation", with: "confirmation" click_button "Save" end end step "I should see a missing password error message" do - page.should have_content "You must provide a valid current password" - end - - step 'I should see the input field yellow' do - page.should have_css 'div.has-warning' - end - - step 'I should see the input field green' do - page.should have_css 'div.has-success' - end - - step 'I should see the input field red' do - page.should have_css 'div.has-error' - end - - step 'I should see the password error message' do - page.should have_content 'Your password is too short' + page.within ".flash-container" do + expect(page).to have_content "You must provide a valid current password" + end end step "I should see a password error message" do - page.should have_content "Password confirmation doesn't match" + page.within '.alert' do + expect(page).to have_content "Password confirmation doesn't match" + end end step 'I reset my token' do - within '.update-token' do + page.within '.update-token' do @old_token = @user.private_token click_button "Reset" end end step 'I should see new token' do - find("#token").value.should_not == @old_token - find("#token").value.should == @user.reload.private_token + expect(find("#token").value).not_to eq @old_token + expect(find("#token").value).to eq @user.reload.private_token end step 'I have activity' do @@ -141,28 +115,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see my activity' do - page.should have_content "#{current_user.name} closed issue" - end - - step "I change my application theme" do - within '.application-theme' do - choose "Violet" - end - end - - step "I change my code preview theme" do - within '.code-preview-theme' do - choose "Solarized dark" - end - end - - step "I should see the theme change immediately" do - page.should have_selector('body.ui_color') - page.should_not have_selector('body.ui_basic') - end - - step "I should receive feedback that the changes were saved" do - page.should have_content("saved") + expect(page).to have_content "#{current_user.name} closed issue" end step 'my password is expired' do @@ -170,42 +123,42 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step "I am not an ldap user" do - current_user.update_attributes(extern_uid: nil, provider: '') - current_user.ldap_user?.should be_false + current_user.identities.delete + expect(current_user.ldap_user?).to eq false end step 'I redirected to expired password page' do - current_path.should == new_profile_password_path + expect(current_path).to eq new_profile_password_path end step 'I submit new password' do fill_in :user_current_password, with: '12345678' - fill_in :user_password_profile, with: '12345678' + fill_in :user_password, with: '12345678' fill_in :user_password_confirmation, with: '12345678' click_button "Set new password" end step 'I redirected to sign in page' do - current_path.should == new_user_session_path + expect(current_path).to eq new_user_session_path end step 'I should be redirected to password page' do - current_path.should == edit_profile_password_path + expect(current_path).to eq edit_profile_password_path end step 'I should be redirected to account page' do - current_path.should == profile_account_path + expect(current_path).to eq profile_account_path end step 'I click on my profile picture' do - click_link 'profile-pic' + find(:css, '.sidebar-user').click end step 'I should see my user page' do - page.should have_content "User Activity" + expect(page).to have_content "User Activity" - within '.navbar-gitlab' do - page.should have_content current_user.name + page.within '.navbar-gitlab' do + expect(page).to have_content current_user.name end end @@ -219,6 +172,56 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see groups I belong to' do - page.should have_css('.profile-groups-avatars', visible: true) + expect(page).to have_css('.profile-groups-avatars', visible: true) + end + + step 'I click on new application button' do + click_on 'New Application' + end + + step 'I should see application form' do + expect(page).to have_content "New Application" + end + + step 'I fill application form out and submit' do + fill_in :doorkeeper_application_name, with: 'test' + fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com' + click_on "Submit" + end + + step 'I see application' do + expect(page).to have_content "Application: test" + expect(page).to have_content "Application Id" + expect(page).to have_content "Secret" + end + + step 'I click edit' do + click_on "Edit" + end + + step 'I see edit application form' do + expect(page).to have_content "Edit application" + end + + step 'I change name of application and submit' do + expect(page).to have_content "Edit application" + fill_in :doorkeeper_application_name, with: 'test_changed' + click_on "Submit" + end + + step 'I see that application was changed' do + expect(page).to have_content "test_changed" + expect(page).to have_content "Application Id" + expect(page).to have_content "Secret" + end + + step 'I click to remove application' do + page.within '.oauth-applications' do + click_on "Destroy" + end + end + + step "I see that application is removed" do + expect(page.find(".oauth-applications")).not_to have_content "test_changed" end end diff --git a/features/steps/profile/ssh_keys.rb b/features/steps/profile/ssh_keys.rb index d1e87d40705..c7f879d247d 100644 --- a/features/steps/profile/ssh_keys.rb +++ b/features/steps/profile/ssh_keys.rb @@ -3,7 +3,7 @@ class Spinach::Features::ProfileSshKeys < Spinach::FeatureSteps step 'I should see my ssh keys' do @user.keys.each do |key| - page.should have_content(key.title) + expect(page).to have_content(key.title) end end @@ -19,9 +19,9 @@ class Spinach::Features::ProfileSshKeys < Spinach::FeatureSteps step 'I should see new ssh key "Laptop"' do key = Key.find_by(title: "Laptop") - page.should have_content(key.title) - page.should have_content(key.key) - current_path.should == profile_key_path(key) + expect(page).to have_content(key.title) + expect(page).to have_content(key.key) + expect(current_path).to eq profile_key_path(key) end step 'I click link "Work"' do @@ -37,9 +37,7 @@ class Spinach::Features::ProfileSshKeys < Spinach::FeatureSteps end step 'I should not see "Work" ssh key' do - within "#keys-table" do - page.should_not have_content "Work" - end + expect(page).not_to have_content "Work" end step 'I have ssh key "ssh-rsa Work"' do diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index bb42d15eae5..fabbc1d3d81 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -20,7 +20,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps end step 'I click the "Edit" tab' do - within '.project-settings-nav' do + page.within '.project-settings-nav' do click_link('Project') end end @@ -93,11 +93,11 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps ensure_active_sub_tab('Issues') end - step 'the active sub tab should be Milestones' do - ensure_active_sub_tab('Milestones') + step 'the active main tab should be Milestones' do + ensure_active_main_tab('Milestones') end - step 'the active sub tab should be Labels' do - ensure_active_sub_tab('Labels') + step 'the active main tab should be Labels' do + ensure_active_main_tab('Labels') end end diff --git a/features/steps/project/archived.rb b/features/steps/project/archived.rb index afbf4d5950d..db1387763d5 100644 --- a/features/steps/project/archived.rb +++ b/features/steps/project/archived.rb @@ -15,15 +15,15 @@ class Spinach::Features::ProjectArchived < Spinach::FeatureSteps When 'I visit project "Forum" page' do project = Project.find_by(name: "Forum") - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I should not see "Archived"' do - page.should_not have_content "Archived" + expect(page).not_to have_content "Archived" end step 'I should see "Archived"' do - page.should have_content "Archived" + expect(page).to have_content "Archived" end When 'I set project archived' do diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb index 07f7e5796a3..338f5e8d3ee 100644 --- a/features/steps/project/commits/branches.rb +++ b/features/steps/project/commits/branches.rb @@ -8,8 +8,8 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps end step 'I should see "Shop" all branches list' do - page.should have_content "Branches" - page.should have_content "master" + expect(page).to have_content "Branches" + expect(page).to have_content "master" end step 'I click link "Protected"' do @@ -17,9 +17,9 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps end step 'I should see "Shop" protected branches list' do - within ".protected-branches-list" do - page.should have_content "stable" - page.should_not have_content "master" + page.within ".protected-branches-list" do + expect(page).to have_content "stable" + expect(page).not_to have_content "master" end end @@ -57,29 +57,29 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps end step 'I should see new branch created' do - page.should have_content 'deploy_keys' + expect(page).to have_content 'deploy_keys' end step 'I should see new an error that branch is invalid' do - page.should have_content 'Branch name invalid' + expect(page).to have_content 'Branch name invalid' end step 'I should see new an error that ref is invalid' do - page.should have_content 'Invalid reference name' + expect(page).to have_content 'Invalid reference name' end step 'I should see new an error that branch already exists' do - page.should have_content 'Branch already exists' + expect(page).to have_content 'Branch already exists' end step "I click branch 'improve/awesome' delete link" do - within '.js-branch-improve\/awesome' do + page.within '.js-branch-improve\/awesome' do find('.btn-remove').click sleep 0.05 end end step "I should not see branch 'improve/awesome'" do - all(visible: true).should_not have_content 'improve/awesome' + expect(page.all(visible: true)).not_to have_content 'improve/awesome' end end diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 935f313e298..e6330ec457e 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -2,34 +2,35 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps include SharedAuthentication include SharedProject include SharedPaths + include SharedDiffNote include RepoHelpers step 'I see project commits' do commit = @project.repository.commit - page.should have_content(@project.name) - page.should have_content(commit.message[0..20]) - page.should have_content(commit.short_id) + expect(page).to have_content(@project.name) + expect(page).to have_content(commit.message[0..20]) + expect(page).to have_content(commit.short_id) end step 'I click atom feed link' do - click_link "Feed" + click_link "Commits Feed" end step 'I see commits atom feed' do commit = @project.repository.commit - response_headers['Content-Type'].should have_content("application/atom+xml") - body.should have_selector("title", text: "Recent commits to #{@project.name}") - body.should have_selector("author email", text: commit.author_email) - body.should have_selector("entry summary", text: commit.description[0..10]) + expect(response_headers['Content-Type']).to have_content("application/atom+xml") + expect(body).to have_selector("title", text: "#{@project.name}:master commits") + expect(body).to have_selector("author email", text: commit.author_email) + expect(body).to have_selector("entry summary", text: commit.description[0..10]) end step 'I click on commit link' do - visit project_commit_path(@project, sample_commit.id) + visit namespace_project_commit_path(@project.namespace, @project, sample_commit.id) end step 'I see commit info' do - page.should have_content sample_commit.message - page.should have_content "Showing #{sample_commit.files_changed_count} changed files" + expect(page).to have_content sample_commit.message + expect(page).to have_content "Showing #{sample_commit.files_changed_count} changed files" end step 'I fill compare fields with refs' do @@ -38,54 +39,57 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps click_button "Compare" end + step 'I unfold diff' do + @diff = first('.js-unfold') + @diff.click + sleep 2 + end + + step 'I should see additional file lines' do + page.within @diff.parent do + expect(first('.new_line').text).not_to have_content "..." + end + end + step 'I see compared refs' do - page.should have_content "Compare View" - page.should have_content "Commits (1)" - page.should have_content "Showing 2 changed files" + expect(page).to have_content "Compare View" + expect(page).to have_content "Commits (1)" + expect(page).to have_content "Showing 2 changed files" end step 'I see breadcrumb links' do - page.should have_selector('ul.breadcrumb') - page.should have_selector('ul.breadcrumb a', count: 4) + expect(page).to have_selector('ul.breadcrumb') + expect(page).to have_selector('ul.breadcrumb a', count: 4) end step 'I see commits stats' do - page.should have_content 'Top 50 Committers' - page.should have_content 'Committers' - page.should have_content 'Total commits' - page.should have_content 'Authors' + expect(page).to have_content 'Top 50 Committers' + expect(page).to have_content 'Committers' + expect(page).to have_content 'Total commits' + expect(page).to have_content 'Authors' end step 'I visit big commit page' do - Commit::DIFF_SAFE_FILES = 20 - visit project_commit_path(@project, sample_big_commit.id) + stub_const('Commit::DIFF_SAFE_FILES', 20) + visit namespace_project_commit_path(@project.namespace, @project, sample_big_commit.id) end step 'I see big commit warning' do - page.should have_content sample_big_commit.message - page.should have_content "Too many changes" - Commit::DIFF_SAFE_FILES = 100 + expect(page).to have_content sample_big_commit.message + expect(page).to have_content "Too many changes" end step 'I visit a commit with an image that changed' do - visit project_commit_path(@project, sample_image_commit.id) + visit namespace_project_commit_path(@project.namespace, @project, sample_image_commit.id) end step 'The diff links to both the previous and current image' do - links = all('.two-up span div a') - links[0]['href'].should =~ %r{blob/#{sample_image_commit.old_blob_id}} - links[1]['href'].should =~ %r{blob/#{sample_image_commit.new_blob_id}} - end - - step 'I click side-by-side diff button' do - click_link "Side-by-side Diff" - end - - step 'I see side-by-side diff button' do - page.should have_content "Side-by-side Diff" + links = page.all('.two-up span div a') + expect(links[0]['href']).to match %r{blob/#{sample_image_commit.old_blob_id}} + expect(links[1]['href']).to match %r{blob/#{sample_image_commit.new_blob_id}} end step 'I see inline diff button' do - page.should have_content "Inline Diff" + expect(page).to have_content "Inline" end end diff --git a/features/steps/project/commits/tags.rb b/features/steps/project/commits/tags.rb index 3465fcbfd07..e6f8faf50fd 100644 --- a/features/steps/project/commits/tags.rb +++ b/features/steps/project/commits/tags.rb @@ -4,8 +4,8 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps include SharedPaths step 'I should see "Shop" all tags list' do - page.should have_content "Tags" - page.should have_content "v1.0.0" + expect(page).to have_content "Tags" + expect(page).to have_content "v1.0.0" end step 'I click new tag link' do @@ -37,37 +37,37 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps end step 'I should see new tag created' do - page.should have_content 'v7.0' + expect(page).to have_content 'v7.0' end step 'I should see new an error that tag is invalid' do - page.should have_content 'Tag name invalid' + expect(page).to have_content 'Tag name invalid' end step 'I should see new an error that tag ref is invalid' do - page.should have_content 'Invalid reference name' + expect(page).to have_content 'Invalid reference name' end step 'I should see new an error that tag already exists' do - page.should have_content 'Tag already exists' + expect(page).to have_content 'Tag already exists' end step "I delete tag 'v1.1.0'" do - within '.tags' do + page.within '.tags' do first('.btn-remove').click sleep 0.05 end end step "I should not see tag 'v1.1.0'" do - within '.tags' do - all(visible: true).should_not have_content 'v1.1.0' + page.within '.tags' do + expect(page.all(visible: true)).not_to have_content 'v1.1.0' end end step 'I delete all tags' do - within '.tags' do - all('.btn-remove').each do |remove| + page.within '.tags' do + page.all('.btn-remove').each do |remove| remove.click sleep 0.05 end @@ -75,8 +75,8 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps end step 'I should see tags info message' do - within '.tags' do - page.should have_content 'Repository has no tags yet.' + page.within '.tags' do + expect(page).to have_content 'Repository has no tags yet.' end end end diff --git a/features/steps/project/commits/user_lookup.rb b/features/steps/project/commits/user_lookup.rb index 0622fef43bb..40cada6da45 100644 --- a/features/steps/project/commits/user_lookup.rb +++ b/features/steps/project/commits/user_lookup.rb @@ -4,11 +4,11 @@ class Spinach::Features::ProjectCommitsUserLookup < Spinach::FeatureSteps include SharedPaths step 'I click on commit link' do - visit project_commit_path(@project, sample_commit.id) + visit namespace_project_commit_path(@project.namespace, @project, sample_commit.id) end step 'I click on another commit link' do - visit project_commit_path(@project, sample_commit.parent_id) + visit namespace_project_commit_path(@project.namespace, @project, sample_commit.parent_id) end step 'I have user with primary email' do @@ -29,9 +29,9 @@ class Spinach::Features::ProjectCommitsUserLookup < Spinach::FeatureSteps def check_author_link(email, user) author_link = find('.commit-author-link') - author_link['href'].should == user_path(user) - author_link['data-original-title'].should == email - find('.commit-author-name').text.should == user.name + expect(author_link['href']).to eq user_path(user) + expect(author_link['data-original-title']).to eq email + expect(find('.commit-author-name').text).to eq user.name end def user_primary diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb index e1062a6ce39..0d39e1997b5 100644 --- a/features/steps/project/create.rb +++ b/features/steps/project/create.rb @@ -3,25 +3,25 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps include SharedPaths step 'fill project form with valid data' do - fill_in 'project_name', with: 'Empty' + fill_in 'project_path', with: 'Empty' click_button "Create project" end step 'I should see project page' do - page.should have_content "Empty" - current_path.should == project_path(Project.last) + expect(page).to have_content "Empty" + expect(current_path).to eq namespace_project_path(Project.last.namespace, Project.last) end step 'I should see empty project instuctions' do - page.should have_content "git init" - page.should have_content "git remote" - page.should have_content Project.last.url_to_repo + 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 - page.should have_content "git init" - page.should have_content "git remote" - page.should have_content Project.last.url_to_repo + 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 @@ -29,7 +29,7 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps end step 'Remote url should update to http link' do - page.should have_content "git remote add origin #{Project.last.http_url_to_repo}" + expect(page).to have_content "git remote add origin #{Project.last.http_url_to_repo}" end step 'If I click on SSH' do @@ -37,6 +37,6 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps end step 'Remote url should update to ssh link' do - page.should have_content "git remote add origin #{Project.last.url_to_repo}" + expect(page).to have_content "git remote add origin #{Project.last.url_to_repo}" end end diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb index 914da31322f..a4d6c9a1b8e 100644 --- a/features/steps/project/deploy_keys.rb +++ b/features/steps/project/deploy_keys.rb @@ -7,9 +7,21 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps create(:deploy_keys_project, project: @project) end - step 'I should see project deploy keys' do - within '.enabled-keys' do - page.should have_content deploy_key.title + step 'I should see project deploy key' do + page.within '.enabled-keys' do + expect(page).to have_content deploy_key.title + end + end + + step 'I should see other project deploy key' do + page.within '.available-keys' do + expect(page).to have_content other_deploy_key.title + end + end + + step 'I should see public deploy key' do + page.within '.available-keys' do + expect(page).to have_content public_deploy_key.title end end @@ -24,23 +36,37 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end step 'I should be on deploy keys page' do - current_path.should == project_deploy_keys_path(@project) + expect(current_path).to eq namespace_project_deploy_keys_path(@project.namespace, @project) end step 'I should see newly created deploy key' do - within '.enabled-keys' do - page.should have_content(deploy_key.title) + page.within '.enabled-keys' do + expect(page).to have_content(deploy_key.title) end end - step 'other project has deploy key' do - @second_project = create :project, namespace: create(:group) + step 'other projects have deploy keys' do + @second_project = create(:project, namespace: create(:group)) @second_project.team << [current_user, :master] create(:deploy_keys_project, project: @second_project) + + @third_project = create(:project, namespace: create(:group)) + @third_project.team << [current_user, :master] + create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first) + end + + step 'I should only see the same deploy key once' do + page.within '.available-keys' do + expect(page).to have_selector('ul li', count: 1) + end + end + + step 'public deploy key exists' do + create(:deploy_key, public: true) end step 'I click attach deploy key' do - within '.available-keys' do + page.within '.available-keys' do click_link 'Enable' end end @@ -50,4 +76,12 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps def deploy_key @project.deploy_keys.last end + + def other_deploy_key + @second_project.deploy_keys.last + end + + def public_deploy_key + DeployKey.are_public.last + end end diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 8e58597db20..0e433781d7a 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -4,8 +4,8 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps include SharedProject step 'I click link "Fork"' do - page.should have_content "Shop" - page.should have_content "Fork" + expect(page).to have_content "Shop" + expect(page).to have_content "Fork" click_link "Fork" end @@ -15,7 +15,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I should see the forked project page' do - page.should have_content "Project was successfully forked." + expect(page).to have_content "Project was successfully forked." end step 'I already have a project named "Shop" in my namespace' do @@ -23,11 +23,11 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I should see a "Name has already been taken" warning' do - page.should have_content "Name has already been taken" + expect(page).to have_content "Name has already been taken" end step 'I fork to my namespace' do - within '.fork-namespaces' do + page.within '.fork-namespaces' do click_link current_user.name end end diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index ccef84cdcc5..78812c52026 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -21,17 +21,17 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I should see merge request "Merge Request On Forked Project"' do - @project.merge_requests.size.should >= 1 + expect(@project.merge_requests.size).to be >= 1 @merge_request = @project.merge_requests.last - current_path.should == project_merge_request_path(@project, @merge_request) - @merge_request.title.should == "Merge Request On Forked Project" - @merge_request.source_project.should == @forked_project - @merge_request.source_branch.should == "fix" - @merge_request.target_branch.should == "master" - page.should have_content @forked_project.path_with_namespace - page.should have_content @project.path_with_namespace - page.should have_content @merge_request.source_branch - page.should have_content @merge_request.target_branch + expect(current_path).to eq namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + expect(@merge_request.title).to eq "Merge Request On Forked Project" + expect(@merge_request.source_project).to eq @forked_project + expect(@merge_request.source_branch).to eq "fix" + expect(@merge_request.target_branch).to eq "master" + expect(page).to have_content @forked_project.path_with_namespace + expect(page).to have_content @project.path_with_namespace + expect(page).to have_content @merge_request.source_branch + expect(page).to have_content @merge_request.target_branch end step 'I fill out a "Merge Request On Forked Project" merge request' do @@ -46,7 +46,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I submit the merge request' do - click_button "Submit merge request" + click_button "Submit new merge request" end step 'I follow the target commit link' do @@ -56,7 +56,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps step 'I should see the commit under the forked from project' do commit = @project.repository.commit - page.should have_content(commit.message) + expect(page).to have_content(commit.message) end step 'I click "Create Merge Request on fork" link' do @@ -64,14 +64,14 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I see prefilled new Merge Request page for the forked project' do - current_path.should == new_project_merge_request_path(@forked_project) - find("#merge_request_source_project_id").value.should == @forked_project.id.to_s - find("#merge_request_target_project_id").value.should == @project.id.to_s - find("#merge_request_source_branch").value.should have_content "new_design" - find("#merge_request_target_branch").value.should have_content "master" - find("#merge_request_title").value.should == "New Design" - verify_commit_link(".mr_target_commit",@project) - verify_commit_link(".mr_source_commit",@forked_project) + expect(current_path).to eq new_namespace_project_merge_request_path(@forked_project.namespace, @forked_project) + expect(find("#merge_request_source_project_id").value).to eq @forked_project.id.to_s + expect(find("#merge_request_target_project_id").value).to eq @project.id.to_s + expect(find("#merge_request_source_branch").value).to have_content "new_design" + expect(find("#merge_request_target_branch").value).to have_content "master" + expect(find("#merge_request_title").value).to eq "New Design" + verify_commit_link(".mr_target_commit", @project) + verify_commit_link(".mr_source_commit", @forked_project) end step 'I update the merge request title' do @@ -83,22 +83,22 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I should see the edited merge request' do - page.should have_content "An Edited Forked Merge Request" - @project.merge_requests.size.should >= 1 + expect(page).to have_content "An Edited Forked Merge Request" + expect(@project.merge_requests.size).to be >= 1 @merge_request = @project.merge_requests.last - current_path.should == project_merge_request_path(@project, @merge_request) - @merge_request.source_project.should == @forked_project - @merge_request.source_branch.should == "fix" - @merge_request.target_branch.should == "master" - page.should have_content @forked_project.path_with_namespace - page.should have_content @project.path_with_namespace - page.should have_content @merge_request.source_branch - page.should have_content @merge_request.target_branch + expect(current_path).to eq namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + expect(@merge_request.source_project).to eq @forked_project + expect(@merge_request.source_branch).to eq "fix" + expect(@merge_request.target_branch).to eq "master" + expect(page).to have_content @forked_project.path_with_namespace + expect(page).to have_content @project.path_with_namespace + expect(page).to have_content @merge_request.source_branch + expect(page).to have_content @merge_request.target_branch end step 'I should see last push widget' do - page.should have_content "You pushed to new_design" - page.should have_link "Create Merge Request" + expect(page).to have_content "You pushed to new_design" + expect(page).to have_link "Create Merge Request" end step 'I click link edit "Merge Request On Forked Project"' do @@ -106,31 +106,46 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I see the edit page prefilled for "Merge Request On Forked Project"' do - current_path.should == edit_project_merge_request_path(@project, @merge_request) - page.should have_content "Edit merge request ##{@merge_request.id}" - find("#merge_request_title").value.should == "Merge Request On Forked Project" + expect(current_path).to eq edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + expect(page).to have_content "Edit merge request ##{@merge_request.id}" + expect(find("#merge_request_title").value).to eq "Merge Request On Forked Project" end step 'I fill out an invalid "Merge Request On Forked Project" merge request' do select "Select branch", from: "merge_request_target_branch" - find(:select, "merge_request_source_project_id", {}).value.should == @forked_project.id.to_s - find(:select, "merge_request_target_project_id", {}).value.should == project.id.to_s - find(:select, "merge_request_source_branch", {}).value.should == "" - find(:select, "merge_request_target_branch", {}).value.should == "" + expect(find(:select, "merge_request_source_project_id", {}).value).to eq @forked_project.id.to_s + expect(find(:select, "merge_request_target_project_id", {}).value).to eq @project.id.to_s + expect(find(:select, "merge_request_source_branch", {}).value).to eq "" + expect(find(:select, "merge_request_target_branch", {}).value).to eq "" click_button "Compare branches" end step 'I should see validation errors' do - page.should have_content "You must select source and target branch" + expect(page).to have_content "You must select source and target branch" end step 'the target repository should be the original repository' do - page.should have_select("merge_request_target_project_id", selected: project.path_with_namespace) + expect(page).to have_select("merge_request_target_project_id", selected: @project.path_with_namespace) + end + + step 'I click "Assign to" dropdown"' do + first('.ajax-users-select').click + end + + step 'I should see the target project ID in the input selector' do + expect(page).to have_selector("input[data-project-id=\"#{@project.id}\"]") + end + + step 'I should see the users from the target project ID' do + expect(page).to have_selector('.user-result', visible: true, count: 2) + users = page.all('.user-name') + expect(users[0].text).to eq 'Unassigned' + expect(users[1].text).to eq @project.users.first.name end # Verify a link is generated against the correct project def verify_commit_link(container_div, container_project) # This should force a wait for the javascript to execute - find(:div,container_div).find(".commit_short_id")['href'].should have_content "#{container_project.path_with_namespace}/commit" + expect(find(:div,container_div).find(".commit_short_id")['href']).to have_content "#{container_project.path_with_namespace}/commit" end end diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index ba460ac8097..5e7e573a6ab 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -3,21 +3,21 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps include SharedProject step 'page should have graphs' do - page.should have_selector ".stat-graph" + expect(page).to have_selector ".stat-graph" end When 'I visit project "Shop" graph page' do project = Project.find_by(name: "Shop") - visit project_graph_path(project, "master") + visit namespace_project_graph_path(project.namespace, project, "master") end step 'I visit project "Shop" commits graph page' do project = Project.find_by(name: "Shop") - visit commits_project_graph_path(project, "master") + visit commits_namespace_project_graph_path(project.namespace, project, "master") end step 'page should have commits graphs' do - page.should have_content "Commits statistic for master" - page.should have_content "Commits per day of month" + expect(page).to have_content "Commit statistics for master" + expect(page).to have_content "Commits per day of month" end end diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb index f4b8d372be8..04e3bf78ede 100644 --- a/features/steps/project/hooks.rb +++ b/features/steps/project/hooks.rb @@ -19,18 +19,18 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps end step 'I should see project hook' do - page.should have_content @hook.url + expect(page).to have_content @hook.url end step 'I submit new hook' do - @url = Faker::Internet.uri("http") + @url = FFaker::Internet.uri("http") fill_in "hook_url", with: @url expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1) end step 'I should see newly created hook' do - current_path.should == project_hooks_path(current_project) - page.should have_content(@url) + expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project) + expect(page).to have_content(@url) end step 'I click test hook button' do @@ -44,19 +44,19 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps end step 'hook should be triggered' do - current_path.should == project_hooks_path(current_project) - page.should have_selector '.flash-notice', + 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.' end step 'I should see hook error message' do - page.should have_selector '.flash-alert', + expect(page).to have_selector '.flash-alert', text: 'Hook execution failed. '\ 'Ensure the project has commits.' end step 'I should see hook service down error message' do - page.should have_selector '.flash-alert', + expect(page).to have_selector '.flash-alert', text: 'Hook execution failed. '\ 'Ensure hook URL is correct and '\ 'service is up.' diff --git a/features/steps/project/issue_tracker.rb b/features/steps/project/issue_tracker.rb deleted file mode 100644 index e1700292701..00000000000 --- a/features/steps/project/issue_tracker.rb +++ /dev/null @@ -1,31 +0,0 @@ -class Spinach::Features::ProjectIssueTracker < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - step 'project "Shop" has issues enabled' do - @project = Project.find_by(name: "Shop") - @project ||= create(:project, name: "Shop", namespace: @user.namespace) - @project.issues_enabled = true - end - - step 'change the issue tracker to "GitLab"' do - select 'GitLab', from: 'project_issues_tracker' - end - - step 'I the project should have "GitLab" as issue tracker' do - find_field('project_issues_tracker').value.should == 'gitlab' - end - - step 'change the issue tracker to "Redmine"' do - select 'Redmine', from: 'project_issues_tracker' - end - - step 'I the project should have "Redmine" as issue tracker' do - find_field('project_issues_tracker').value.should == 'redmine' - end - - step 'I save project' do - click_button 'Save changes' - end -end diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb index e62fa9c84c8..50bb32429b9 100644 --- a/features/steps/project/issues/filter_labels.rb +++ b/features/steps/project/issues/filter_labels.rb @@ -2,57 +2,38 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps include SharedAuthentication include SharedProject include SharedPaths - - step 'I should see "bug" in labels filter' do - within ".labels-filter" do - page.should have_content "bug" - end - end - - step 'I should see "feature" in labels filter' do - within ".labels-filter" do - page.should have_content "feature" - end - end - - step 'I should see "enhancement" in labels filter' do - within ".labels-filter" do - page.should have_content "enhancement" - end - end + include Select2Helper step 'I should see "Bugfix1" in issues list' do - within ".issues-list" do - page.should have_content "Bugfix1" + page.within ".issues-list" do + expect(page).to have_content "Bugfix1" end end step 'I should see "Bugfix2" in issues list' do - within ".issues-list" do - page.should have_content "Bugfix2" + page.within ".issues-list" do + expect(page).to have_content "Bugfix2" end end step 'I should not see "Bugfix2" in issues list' do - within ".issues-list" do - page.should_not have_content "Bugfix2" + page.within ".issues-list" do + expect(page).not_to have_content "Bugfix2" end end step 'I should not see "Feature1" in issues list' do - within ".issues-list" do - page.should_not have_content "Feature1" + page.within ".issues-list" do + expect(page).not_to have_content "Feature1" end end step 'I click link "bug"' do - within ".labels-filter" do - click_link "bug" - end + select2('bug', from: "#label_name") end step 'I click link "feature"' do - within ".labels-filter" do + page.within ".labels-filter" do click_link "feature" end end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 640603562dd..6873c043e19 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -1,32 +1,46 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps include SharedAuthentication + include SharedIssuable include SharedProject include SharedNote include SharedPaths include SharedMarkdown step 'I should see "Release 0.4" in issues' do - page.should have_content "Release 0.4" + expect(page).to have_content "Release 0.4" end step 'I should not see "Release 0.3" in issues' do - page.should_not have_content "Release 0.3" + expect(page).not_to have_content "Release 0.3" end step 'I should not see "Tweet control" in issues' do - page.should_not have_content "Tweet control" + expect(page).not_to have_content "Tweet control" + end + + step 'I should see that I am subscribed' do + expect(find(".subscribe-button span").text).to eq "Unsubscribe" + end + + step 'I should see that I am unsubscribed' do + sleep 0.2 + expect(find(".subscribe-button span").text).to eq "Subscribe" end step 'I click link "Closed"' do click_link "Closed" end + step 'I click button "Unsubscribe"' do + click_on "Unsubscribe" + end + step 'I should see "Release 0.3" in issues' do - page.should have_content "Release 0.3" + expect(page).to have_content "Release 0.3" end step 'I should not see "Release 0.4" in issues' do - page.should_not have_content "Release 0.4" + expect(page).not_to have_content "Release 0.4" end step 'I click link "All"' do @@ -38,13 +52,25 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I should see issue "Release 0.4"' do - page.should have_content "Release 0.4" + expect(page).to have_content "Release 0.4" end step 'I click link "New Issue"' do click_link "New Issue" end + step 'I click "author" dropdown' do + first('.ajax-users-select').click + end + + step 'I see current user as the first user' do + expect(page).to have_selector('.user-result', visible: true, count: 4) + users = page.all('.user-name') + expect(users[0].text).to eq 'Any' + expect(users[1].text).to eq 'Unassigned' + expect(users[2].text).to eq current_user.name + end + step 'I submit new issue "500 error on profile"' do fill_in "issue_title", with: "500 error on profile" click_button "Submit new issue" @@ -61,16 +87,16 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I should see label \'bug\' with issue' do - within '.issue-show-labels' do - page.should have_content 'bug' + page.within '.issue-show-labels' do + expect(page).to have_content 'bug' end end step 'I should see issue "500 error on profile"' do issue = Issue.find_by(title: "500 error on profile") - page.should have_content issue.title - page.should have_content issue.author_name - page.should have_content issue.project.name + expect(page).to have_content issue.title + expect(page).to have_content issue.author_name + expect(page).to have_content issue.project.name end step 'I fill in issue search with "Re"' do @@ -113,7 +139,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps step 'I should see selected milestone with title "v3.0"' do issues_milestone_selector = "#issue_milestone_id_chzn > a" - find(issues_milestone_selector).should have_content("v3.0") + expect(find(issues_milestone_selector)).to have_content("v3.0") end When 'I select first assignee from "Shop" project' do @@ -126,7 +152,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps issues_assignee_selector = "#issue_assignee_id_chzn > a" assignee_name = project.users.first.name - find(issues_assignee_selector).should have_content(assignee_name) + expect(find(issues_assignee_selector)).to have_content(assignee_name) end step 'project "Shop" have "Release 0.4" open issue' do @@ -153,45 +179,43 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps author: project.users.first) end - step 'project "Shop" has "Tasks-open" open issue with task markdown' do - create_taskable(:issue, 'Tasks-open') - end - - step 'project "Shop" has "Tasks-closed" closed issue with task markdown' do - create_taskable(:closed_issue, 'Tasks-closed') - end - step 'empty project "Empty Project"' do create :empty_project, name: 'Empty Project', namespace: @user.namespace end When 'I visit empty project page' do project = Project.find_by(name: 'Empty Project') - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I see empty project details with ssh clone info' do project = Project.find_by(name: 'Empty Project') - all(:css, '.git-empty .clone').each do |element| - element.text.should include(project.url_to_repo) + page.all(:css, '.git-empty .clone').each do |element| + expect(element.text).to include(project.url_to_repo) end end When "I visit empty project's issues page" do project = Project.find_by(name: 'Empty Project') - visit project_issues_path(project) + visit namespace_project_issues_path(project.namespace, project) end step 'I leave a comment with code block' do - within(".js-main-target-form") do + page.within(".js-main-target-form") do fill_in "note[note]", with: "```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```" click_button "Add Comment" sleep 0.05 end end + step 'I should see an error alert section within the comment form' do + page.within(".js-main-target-form") do + find(".error-alert") + end + end + step 'The code block should be unchanged' do - page.should have_content("```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```") + expect(page).to have_content("```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```") end step 'project \'Shop\' has issue \'Bugfix1\' with description: \'Description for issue1\'' do @@ -215,15 +239,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I should see \'Bugfix1\' in issues' do - page.should have_content 'Bugfix1' + expect(page).to have_content 'Bugfix1' end step 'I should see \'Feature1\' in issues' do - page.should have_content 'Feature1' + expect(page).to have_content 'Feature1' end step 'I should not see \'Bugfix1\' in issues' do - page.should_not have_content 'Bugfix1' + expect(page).not_to have_content 'Bugfix1' end step 'issue \'Release 0.4\' has label \'bug\'' do @@ -233,7 +257,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I click label \'bug\'' do - within ".issues-list" do + page.within ".issues-list" do click_link 'bug' end end diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index 3e3e90824b4..d656acf4220 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -4,18 +4,18 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps include SharedPaths step 'I visit \'bug\' label edit page' do - visit edit_project_label_path(project, bug_label) + visit edit_namespace_project_label_path(project.namespace, project, bug_label) end step 'I remove label \'bug\'' do - within "#label_#{bug_label.id}" do + page.within "#label_#{bug_label.id}" do click_link 'Remove' end end step 'I delete all labels' do - within '.labels' do - all('.btn-remove').each do |remove| + page.within '.labels' do + page.all('.btn-remove').each do |remove| remove.click sleep 0.05 end @@ -23,8 +23,8 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps end step 'I should see labels help message' do - within '.labels' do - page.should have_content 'Create first label or generate default set of '\ + page.within '.labels' do + expect(page).to have_content 'Create first label or generate default set of '\ 'labels' end end @@ -48,38 +48,38 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps end step 'I should see label label exist error message' do - within '.label-form' do - page.should have_content 'Title has already been taken' + page.within '.label-form' do + expect(page).to have_content 'Title has already been taken' end end step 'I should see label color error message' do - within '.label-form' do - page.should have_content 'Color is invalid' + page.within '.label-form' do + expect(page).to have_content 'Color is invalid' end end step 'I should see label \'feature\'' do - within '.manage-labels-list' do - page.should have_content 'feature' + page.within '.manage-labels-list' do + expect(page).to have_content 'feature' end end step 'I should see label \'bug\'' do - within '.manage-labels-list' do - page.should have_content 'bug' + page.within '.manage-labels-list' do + expect(page).to have_content 'bug' end end step 'I should not see label \'bug\'' do - within '.manage-labels-list' do - page.should_not have_content 'bug' + page.within '.manage-labels-list' do + expect(page).not_to have_content 'bug' end end step 'I should see label \'support\'' do - within '.manage-labels-list' do - page.should have_content 'support' + page.within '.manage-labels-list' do + expect(page).to have_content 'support' end end @@ -90,8 +90,8 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps end step 'I should see label \'fix\'' do - within '.manage-labels-list' do - page.should have_content 'fix' + page.within '.manage-labels-list' do + expect(page).to have_content 'fix' end end diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb index cce87a6d981..708c5243947 100644 --- a/features/steps/project/issues/milestones.rb +++ b/features/steps/project/issues/milestones.rb @@ -6,9 +6,9 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps step 'I should see milestone "v2.2"' do milestone = @project.milestones.find_by(title: "v2.2") - page.should have_content(milestone.title[0..10]) - page.should have_content(milestone.expires_at) - page.should have_content("Issues") + expect(page).to have_content(milestone.title[0..10]) + expect(page).to have_content(milestone.expires_at) + expect(page).to have_content("Issues") end step 'I click link "v2.2"' do @@ -26,9 +26,9 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps step 'I should see milestone "v2.3"' do milestone = @project.milestones.find_by(title: "v2.3") - page.should have_content(milestone.title[0..10]) - page.should have_content(milestone.expires_at) - page.should have_content("Issues") + expect(page).to have_content(milestone.title[0..10]) + expect(page).to have_content(milestone.expires_at) + expect(page).to have_content("Issues") end step 'project "Shop" has milestone "v2.2"' do @@ -54,6 +54,6 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps end step 'I should see 3 issues' do - page.should have_selector('#tab-issues li.issue-row', count: 4) + expect(page).to have_selector('#tab-issues li.issue-row', count: 4) end end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index fae0cec53a6..b072fef235e 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -1,5 +1,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps include SharedAuthentication + include SharedIssuable include SharedProject include SharedNote include SharedPaths @@ -18,47 +19,57 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps click_link "All" end - step 'I click link "Closed"' do - click_link "Closed" + step 'I click link "Rejected"' do + click_link "Rejected" end step 'I should see merge request "Wiki Feature"' do - within '.merge-request' do - page.should have_content "Wiki Feature" + page.within '.merge-request' do + expect(page).to have_content "Wiki Feature" end end step 'I should see closed merge request "Bug NS-04"' do merge_request = MergeRequest.find_by!(title: "Bug NS-04") - merge_request.closed?.should be_true - page.should have_content "Closed by" + expect(merge_request).to be_closed + expect(page).to have_content 'Rejected by' end step 'I should see merge request "Bug NS-04"' do - page.should have_content "Bug NS-04" + expect(page).to have_content "Bug NS-04" end step 'I should see "Bug NS-04" in merge requests' do - page.should have_content "Bug NS-04" + expect(page).to have_content "Bug NS-04" end step 'I should see "Feature NS-03" in merge requests' do - page.should have_content "Feature NS-03" + expect(page).to have_content "Feature NS-03" end step 'I should not see "Feature NS-03" in merge requests' do - page.should_not have_content "Feature NS-03" + expect(page).not_to have_content "Feature NS-03" end step 'I should not see "Bug NS-04" in merge requests' do - page.should_not have_content "Bug NS-04" + expect(page).not_to have_content "Bug NS-04" + end + + step 'I should see that I am subscribed' do + expect(find(".subscribe-button span").text).to eq "Unsubscribe" + end + + step 'I should see that I am unsubscribed' do + expect(find(".subscribe-button span")).to have_content("Subscribe") + end + + step 'I click button "Unsubscribe"' do + click_on "Unsubscribe" end step 'I click link "Close"' do - within '.page-title' do - click_link "Close" - end + first(:css, '.close-mr-link').click end step 'I submit new merge request "Wiki Feature"' do @@ -66,7 +77,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps select "feature", from: "merge_request_target_branch" click_button "Compare branches" fill_in "merge_request_title", with: "Wiki Feature" - click_button "Submit merge request" + click_button "Submit new merge request" end step 'project "Shop" have "Bug NS-04" open merge request' do @@ -97,20 +108,37 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps author: project.users.first) end - step 'project "Shop" has "MR-task-open" open MR with task markdown' do - create_taskable(:merge_request, 'MR-task-open') + step 'I switch to the diff tab' do + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) end - step 'I switch to the diff tab' do - visit diffs_project_merge_request_path(project, merge_request) + step 'I click on the Changes tab via Javascript' do + page.within '.merge-request-tabs' do + click_link 'Changes' + end + + sleep 2 + end + + step 'I should see the proper Inline and Side-by-side links' do + buttons = page.all('#commit-diff-viewtype') + expect(buttons.count).to eq(2) + + buttons.each do |b| + expect(b['href']).not_to have_content('json') + end end step 'I switch to the merge request\'s comments tab' do - visit project_merge_request_path(project, merge_request) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) end step 'I click on the commit in the merge request' do - within '.mr-commits' do + page.within '.merge-request-tabs' do + click_link 'Commits' + end + + page.within '.commits' do click_link Commit.truncate_sha(sample_commit.id) end end @@ -136,24 +164,30 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see a discussion has started on diff' do - page.should have_content "#{current_user.name} started a discussion" - page.should have_content sample_commit.line_code_path - page.should have_content "Line is wrong" + page.within(".notes .discussion") do + page.should have_content "#{current_user.name} started a discussion" + page.should have_content sample_commit.line_code_path + page.should have_content "Line is wrong" + end end step 'I should see a discussion has started on commit diff' do - page.should have_content "#{current_user.name} started a discussion on commit" - page.should have_content sample_commit.line_code_path - page.should have_content "Line is wrong" + page.within(".notes .discussion") do + page.should have_content "#{current_user.name} started a discussion on commit" + page.should have_content sample_commit.line_code_path + page.should have_content "Line is wrong" + end end step 'I should see a discussion has started on commit' do - page.should have_content "#{current_user.name} started a discussion on commit" - page.should have_content "One comment to rule them all" + page.within(".notes .discussion") do + page.should have_content "#{current_user.name} started a discussion on commit" + page.should have_content "One comment to rule them all" + end end step 'merge request is mergeable' do - page.should have_content 'You can accept this request automatically' + expect(page).to have_button 'Accept Merge Request' end step 'I modify merge commit message' do @@ -162,6 +196,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'merge request "Bug NS-05" is mergeable' do + merge_request.project.satellite.create merge_request.mark_as_mergeable end @@ -170,88 +205,88 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps merge!: true, ) - click_button "Accept Merge Request" + page.within '.mr-state-widget' do + click_button "Accept Merge Request" + end end step 'I should see merged request' do - within '.issue-box' do - page.should have_content "Merged" + page.within '.issue-box' do + expect(page).to have_content "Accepted" end end step 'I click link "Reopen"' do - within '.page-title' do - click_link "Reopen" - end + first(:css, '.reopen-mr-link').click end step 'I should see reopened merge request "Bug NS-04"' do - within '.state-label' do - page.should have_content "Open" + page.within '.issue-box' do + expect(page).to have_content "Open" end end step 'I click link "Hide inline discussion" of the second file' do - within '.files [id^=diff]:nth-child(2)' do - click_link "Diff comments" + page.within '.files [id^=diff]:nth-child(2)' do + find('.js-toggle-diff-comments').click end end step 'I click link "Show inline discussion" of the second file' do - within '.files [id^=diff]:nth-child(2)' do - click_link "Diff comments" + page.within '.files [id^=diff]:nth-child(2)' do + find('.js-toggle-diff-comments').click end end step 'I should not see a comment like "Line is wrong" in the second file' do - within '.files [id^=diff]:nth-child(2)' do - page.should_not have_visible_content "Line is wrong" + page.within '.files [id^=diff]:nth-child(2)' do + expect(page).not_to have_visible_content "Line is wrong" end end step 'I should see a comment like "Line is wrong" in the second file' do - within '.files [id^=diff]:nth-child(2) .note-text' do - page.should have_visible_content "Line is wrong" + page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do + expect(page).to have_visible_content "Line is wrong" end end step 'I should not see a comment like "Line is wrong here" in the second file' do - within '.files [id^=diff]:nth-child(2)' do - page.should_not have_visible_content "Line is wrong here" + page.within '.files [id^=diff]:nth-child(2)' do + expect(page).not_to have_visible_content "Line is wrong here" end end step 'I should see a comment like "Line is wrong here" in the second file' do - within '.files [id^=diff]:nth-child(2) .note-text' do - page.should have_visible_content "Line is wrong here" + page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do + expect(page).to have_visible_content "Line is wrong here" end end step 'I leave a comment like "Line is correct" on line 12 of the first file' do init_diff_note_first_file - within(".js-discussion-note-form") do + page.within(".js-discussion-note-form") do fill_in "note_note", with: "Line is correct" click_button "Add Comment" end - within ".files [id^=diff]:nth-child(1) .note-text" do - page.should have_content "Line is correct" + page.within ".files [id^=diff]:nth-child(1) .note-body > .note-text" do + expect(page).to have_content "Line is correct" end end step 'I leave a comment like "Line is wrong" on line 39 of the second file' do init_diff_note_second_file - within(".js-discussion-note-form") do + page.within(".js-discussion-note-form") do fill_in "note_note", with: "Line is wrong on here" click_button "Add Comment" end end step 'I should still see a comment like "Line is correct" in the first file' do - within '.files [id^=diff]:nth-child(1) .note-text' do - page.should have_visible_content "Line is correct" + page.within '.files [id^=diff]:nth-child(1) .note-body > .note-text' do + expect(page).to have_visible_content "Line is correct" end end @@ -264,15 +299,33 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I click Side-by-side Diff tab' do - click_link 'Side-by-side Diff' + find('a', text: 'Side-by-side').trigger('click') end step 'I should see comments on the side-by-side diff page' do - within '.files [id^=diff]:nth-child(1) .note-text' do - page.should have_visible_content "Line is correct" + page.within '.files [id^=diff]:nth-child(1) .parallel .note-body > .note-text' do + expect(page).to have_visible_content "Line is correct" end end + step 'I fill in merge request search with "Fe"' do + fill_in 'issue_search', with: "Fe" + end + + step 'I click the "Target branch" dropdown' do + first('.target_branch').click + end + + step 'I select a new target branch' do + select "feature", from: "merge_request_target_branch" + click_button 'Save' + end + + step 'I should see new target branch changes' do + expect(page).to have_content 'From fix into feature' + expect(page).to have_content 'Target branch changed from master to feature' + end + def merge_request @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") end @@ -282,12 +335,13 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end def leave_comment(message) - within(".js-discussion-note-form") do + page.within(".js-discussion-note-form", visible: true) do fill_in "note_note", with: message click_button "Add Comment" end - - page.should have_content message + page.within(".notes_holder", visible: true) do + expect(page).to have_content message + end end def init_diff_note_first_file diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb index 14fdc72b8b6..992cf2734fd 100644 --- a/features/steps/project/network_graph.rb +++ b/features/steps/project/network_graph.rb @@ -4,7 +4,7 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps include SharedProject step 'page should have network graph' do - page.should have_selector ".network-graph" + expect(page).to have_selector ".network-graph" end When 'I visit project "Shop" network page' do @@ -12,20 +12,20 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps Network::Graph.stub(max_count: 10) project = Project.find_by(name: "Shop") - visit project_network_path(project, "master") + visit namespace_project_network_path(project.namespace, project, "master") end step 'page should select "master" in select box' do - page.should have_selector '.select2-chosen', text: "master" + expect(page).to have_selector '.select2-chosen', text: "master" end step 'page should select "v1.0.0" in select box' do - page.should have_selector '.select2-chosen', text: "v1.0.0" + expect(page).to have_selector '.select2-chosen', text: "v1.0.0" end step 'page should have "master" on graph' do - within '.network-graph' do - page.should have_content 'master' + page.within '.network-graph' do + expect(page).to have_content 'master' end end @@ -45,33 +45,33 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end step 'page should have content not containing "v1.0.0"' do - within '.network-graph' do - page.should have_content 'Change some files' + page.within '.network-graph' do + expect(page).to have_content 'Change some files' end end step 'page should not have content not containing "v1.0.0"' do - within '.network-graph' do - page.should_not have_content 'Change some files' + page.within '.network-graph' do + expect(page).not_to have_content 'Change some files' end end step 'page should select "feature" in select box' do - page.should have_selector '.select2-chosen', text: "feature" + expect(page).to have_selector '.select2-chosen', text: "feature" end step 'page should select "v1.0.0" in select box' do - page.should have_selector '.select2-chosen', text: "v1.0.0" + expect(page).to have_selector '.select2-chosen', text: "v1.0.0" end step 'page should have "feature" on graph' do - within '.network-graph' do - page.should have_content 'feature' + page.within '.network-graph' do + expect(page).to have_content 'feature' end end When 'I looking for a commit by SHA of "v1.0.0"' do - within ".network-form" do + page.within ".network-form" do fill_in 'extended_sha1', with: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' find('button').click end @@ -79,13 +79,13 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end step 'page should have "v1.0.0" on graph' do - within '.network-graph' do - page.should have_content 'v1.0.0' + page.within '.network-graph' do + expect(page).to have_content 'v1.0.0' end end When 'I look for a commit by ";"' do - within ".network-form" do + page.within ".network-form" do fill_in 'extended_sha1', with: ';' find('button').click end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 5e7312d90ff..e4465a1c3b7 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -13,21 +13,62 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see project with new settings' do - find_field('project_name').value.should == 'NewName' + expect(find_field('project_name').value).to eq 'NewName' end step 'change project path settings' do - fill_in "project_path", with: "new-path" - click_button "Rename" + fill_in 'project_path', with: 'new-path' + click_button 'Rename' end step 'I should see project with new path settings' do - project.path.should == "new-path" + expect(project.path).to eq 'new-path' + end + + step 'I change the project avatar' do + attach_file( + :project_avatar, + File.join(Rails.root, 'public', 'gitlab_logo.png') + ) + click_button 'Save changes' + @project.reload + end + + step 'I should see new project avatar' do + expect(@project.avatar).to be_instance_of AvatarUploader + url = @project.avatar.url + expect(url).to eq "/uploads/project/avatar/#{ @project.id }/gitlab_logo.png" + end + + step 'I should see the "Remove avatar" button' do + expect(page).to have_link('Remove avatar') + end + + step 'I have an project avatar' do + attach_file( + :project_avatar, + File.join(Rails.root, 'public', 'gitlab_logo.png') + ) + click_button 'Save changes' + @project.reload + end + + step 'I remove my project avatar' do + click_link 'Remove avatar' + @project.reload + end + + step 'I should see the default project avatar' do + expect(@project.avatar?).to eq false + end + + step 'I should not see the "Remove avatar" button' do + expect(page).not_to have_link('Remove avatar') end step 'I should see project "Shop" version' do - within '.project-side' do - page.should have_content "Version: 6.7.0.pre" + page.within '.project-side' do + expect(page).to have_content '6.7.0.pre' end end @@ -37,7 +78,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see project default branch changed' do - find(:css, 'select#project_default_branch').value.should == 'fix' + expect(find(:css, 'select#project_default_branch').value).to eq 'fix' end step 'I select project "Forum" README tab' do @@ -45,12 +86,32 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see project "Forum" README' do - page.should have_link "README.md" - page.should have_content "Sample repo for testing gitlab features" + expect(page).to have_link 'README.md' + expect(page).to have_content 'Sample repo for testing gitlab features' end step 'I should see project "Shop" README' do - page.should have_link "README.md" - page.should have_content "testme" + expect(page).to have_link 'README.md' + expect(page).to have_content 'testme' + end + + step 'I add project tags' do + fill_in 'Tags', with: 'tag1, tag2' + end + + step 'I should see project tags' do + expect(find_field('Tags').value).to eq 'tag1, tag2' + end + + step 'I should not see "New Issue" button' do + expect(page).not_to have_link 'New Issue' + end + + step 'I should not see "New Merge Request" button' do + expect(page).not_to have_link 'New Merge Request' + end + + step 'I should not see "Snippets" button' do + expect(page).not_to have_link 'Snippets' end end diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb index e54637120ce..0e724138a8a 100644 --- a/features/steps/project/redirects.rb +++ b/features/steps/project/redirects.rb @@ -13,24 +13,24 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps step 'I visit project "Community" page' do project = Project.find_by(name: 'Community') - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I should see project "Community" home page' do - Gitlab.config.gitlab.stub(:host).and_return("www.example.com") - within '.navbar-gitlab .title' do - page.should have_content 'Community' + Gitlab.config.gitlab.should_receive(:host).and_return("www.example.com") + page.within '.navbar-gitlab .title' do + expect(page).to have_content 'Community' end end step 'I visit project "Enterprise" page' do project = Project.find_by(name: 'Enterprise') - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I visit project "CommunityDoesNotExist" page' do project = Project.find_by(name: 'Community') - visit project_path(project) + 'DoesNotExist' + visit namespace_project_path(project.namespace, project) + 'DoesNotExist' end step 'I click on "Sign In"' do @@ -48,8 +48,8 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps step 'I should be redirected to "Community" page' do project = Project.find_by(name: 'Community') - current_path.should == "/#{project.path_with_namespace}" - status_code.should == 200 + expect(current_path).to eq "/#{project.path_with_namespace}" + expect(status_code).to eq 200 end step 'I get redirected to signin page where I sign in' do @@ -63,7 +63,7 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps step 'I should be redirected to "Enterprise" page' do project = Project.find_by(name: 'Enterprise') - current_path.should == "/#{project.path_with_namespace}" - status_code.should == 200 + expect(current_path).to eq "/#{project.path_with_namespace}" + expect(status_code).to eq 200 end end diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 7a0b47a8fe5..0327fd61981 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -4,17 +4,20 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps include SharedPaths step 'I visit project "Shop" services page' do - visit project_services_path(@project) + visit namespace_project_services_path(@project.namespace, @project) end step 'I should see list of available services' do - page.should have_content 'Project services' - page.should have_content 'Campfire' - page.should have_content 'HipChat' - page.should have_content 'GitLab CI' - page.should have_content 'Assembla' - page.should have_content 'Pushover' - page.should have_content 'Atlassian Bamboo' + expect(page).to have_content 'Project services' + expect(page).to have_content 'Campfire' + expect(page).to have_content 'HipChat' + expect(page).to have_content 'GitLab CI' + expect(page).to have_content 'Assembla' + expect(page).to have_content 'Pushover' + expect(page).to have_content 'Atlassian Bamboo' + expect(page).to have_content 'JetBrains TeamCity' + expect(page).to have_content 'Asana' + expect(page).to have_content 'Irker (IRC gateway)' end step 'I click gitlab-ci service link' do @@ -29,7 +32,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see service settings saved' do - find_field('Project url').value.should == 'http://ci.gitlab.org/projects/3' + expect(find_field('Project url').value).to eq 'http://ci.gitlab.org/projects/3' end step 'I click hipchat service link' do @@ -44,7 +47,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see hipchat service settings saved' do - find_field('Room').value.should == 'gitlab' + expect(find_field('Room').value).to eq 'gitlab' end step 'I fill hipchat settings with custom server' do @@ -56,7 +59,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see hipchat service settings with custom server saved' do - find_field('Server').value.should == 'https://chat.example.com' + expect(find_field('Server').value).to eq 'https://chat.example.com' end step 'I click pivotaltracker service link' do @@ -70,7 +73,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see pivotaltracker service settings saved' do - find_field('Token').value.should == 'verySecret' + expect(find_field('Token').value).to eq 'verySecret' end step 'I click Flowdock service link' do @@ -84,7 +87,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see Flowdock service settings saved' do - find_field('Token').value.should == 'verySecret' + expect(find_field('Token').value).to eq 'verySecret' end step 'I click Assembla service link' do @@ -98,7 +101,23 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see Assembla service settings saved' do - find_field('Token').value.should == 'verySecret' + expect(find_field('Token').value).to eq 'verySecret' + end + + step 'I click Asana service link' do + click_link 'Asana' + end + + step 'I fill Asana settings' do + check 'Active' + fill_in 'Api key', with: 'verySecret' + fill_in 'Restrict to branch', with: 'master' + click_button 'Save' + end + + step 'I should see Asana service settings saved' do + expect(find_field('Api key').value).to eq 'verySecret' + expect(find_field('Restrict to branch').value).to eq 'master' end step 'I click email on push service link' do @@ -111,7 +130,23 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see email on push service settings saved' do - find_field('Recipients').value.should == 'qa@company.name' + expect(find_field('Recipients').value).to eq 'qa@company.name' + end + + step 'I click Irker service link' do + click_link 'Irker (IRC gateway)' + end + + step 'I fill Irker settings' do + check 'Active' + fill_in 'Recipients', with: 'irc://chat.freenode.net/#commits' + check 'Colorize messages' + click_button 'Save' + end + + step 'I should see Irker service settings saved' do + expect(find_field('Recipients').value).to eq 'irc://chat.freenode.net/#commits' + expect(find_field('Colorize messages').value).to eq '1' end step 'I click Slack service link' do @@ -125,7 +160,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see Slack service settings saved' do - find_field('Webhook').value.should == 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' + expect(find_field('Webhook').value).to eq 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' end step 'I click Pushover service link' do @@ -143,11 +178,11 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see Pushover service settings saved' do - find_field('Api key').value.should == 'verySecret' - find_field('User key').value.should == 'verySecret' - find_field('Device').value.should == 'myDevice' - find_field('Priority').find('option[selected]').value.should == '1' - find_field('Sound').find('option[selected]').value.should == 'bike' + expect(find_field('Api key').value).to eq 'verySecret' + expect(find_field('User key').value).to eq 'verySecret' + expect(find_field('Device').value).to eq 'myDevice' + expect(find_field('Priority').find('option[selected]').value).to eq '1' + expect(find_field('Sound').find('option[selected]').value).to eq 'bike' end step 'I click Atlassian Bamboo CI service link' do @@ -164,8 +199,27 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see Atlassian Bamboo CI service settings saved' do - find_field('Bamboo url').value.should == 'http://bamboo.example.com' - find_field('Build key').value.should == 'KEY' - find_field('Username').value.should == 'user' + expect(find_field('Bamboo url').value).to eq 'http://bamboo.example.com' + expect(find_field('Build key').value).to eq 'KEY' + expect(find_field('Username').value).to eq 'user' + end + + step 'I click JetBrains TeamCity CI service link' do + click_link 'JetBrains TeamCity CI' + end + + step 'I fill JetBrains TeamCity CI settings' do + check 'Active' + fill_in 'Teamcity url', with: 'http://teamcity.example.com' + fill_in 'Build type', with: 'GitlabTest_Build' + fill_in 'Username', with: 'user' + fill_in 'Password', with: 'verySecret' + click_button 'Save' + end + + step 'I should see JetBrains TeamCity CI service settings saved' do + expect(find_field('Teamcity url').value).to eq 'http://teamcity.example.com' + expect(find_field('Build type').value).to eq 'GitlabTest_Build' + expect(find_field('Username').value).to eq 'user' end end diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index 4a39bfdbb79..eedb4e1b74a 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -30,19 +30,19 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I should see "Snippet one" in snippets' do - page.should have_content "Snippet one" + expect(page).to have_content "Snippet one" end step 'I should not see "Snippet two" in snippets' do - page.should_not have_content "Snippet two" + expect(page).not_to have_content "Snippet two" end step 'I should not see "Snippet one" in snippets' do - page.should_not have_content "Snippet one" + expect(page).not_to have_content "Snippet one" end step 'I click link "Edit"' do - within ".file-title" do + page.within ".file-title" do click_link "Edit" end end @@ -54,15 +54,15 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps step 'I submit new snippet "Snippet three"' do fill_in "project_snippet_title", :with => "Snippet three" fill_in "project_snippet_file_name", :with => "my_snippet.rb" - within('.file-editor') do + page.within('.file-editor') do find(:xpath, "//input[@id='project_snippet_content']").set 'Content of snippet three' end click_button "Create snippet" end step 'I should see snippet "Snippet three"' do - page.should have_content "Snippet three" - page.should have_content "Content of snippet three" + expect(page).to have_content "Snippet three" + expect(page).to have_content "Content of snippet three" end step 'I submit new title "Snippet new title"' do @@ -71,22 +71,22 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I should see "Snippet new title"' do - page.should have_content "Snippet new title" + expect(page).to have_content "Snippet new title" end step 'I leave a comment like "Good snippet!"' do - within('.js-main-target-form') do + page.within('.js-main-target-form') do fill_in "note_note", with: "Good snippet!" click_button "Add Comment" end end step 'I should see comment "Good snippet!"' do - page.should have_content "Good snippet!" + expect(page).to have_content "Good snippet!" end step 'I visit snippet page "Snippet one"' do - visit project_snippet_path(project, project_snippet) + visit namespace_project_snippet_path(project.namespace, project, project_snippet) end def project_snippet diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index ddd501d4f88..398c9bf5756 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -5,23 +5,23 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps include RepoHelpers step 'I should see files from repository' do - page.should have_content "VERSION" - page.should have_content ".gitignore" - page.should have_content "LICENSE" + expect(page).to have_content "VERSION" + expect(page).to have_content ".gitignore" + expect(page).to have_content "LICENSE" end step 'I should see files from repository for "6d39438"' do - current_path.should == project_tree_path(@project, "6d39438") - page.should have_content ".gitignore" - page.should have_content "LICENSE" + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "6d39438") + expect(page).to have_content ".gitignore" + expect(page).to have_content "LICENSE" end step 'I see the ".gitignore"' do - page.should have_content '.gitignore' + expect(page).to have_content '.gitignore' end step 'I don\'t see the ".gitignore"' do - page.should_not have_content '.gitignore' + expect(page).not_to have_content '.gitignore' end step 'I click on ".gitignore" file in repo' do @@ -29,11 +29,11 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I should see its content' do - page.should have_content old_gitignore_content + expect(page).to have_content old_gitignore_content end step 'I should see its new content' do - page.should have_content new_gitignore_content + expect(page).to have_content new_gitignore_content end step 'I click link "Raw"' do @@ -41,16 +41,24 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I should see raw file content' do - source.should == sample_blob.data + expect(source).to eq sample_blob.data end step 'I click button "Edit"' do click_link 'Edit' end + step 'I cannot see the edit button' do + expect(page).not_to have_link 'edit' + end + + step 'The edit button is disabled' do + expect(page).to have_css '.disabled', text: 'Edit' + end + step 'I can edit code' do set_new_content - evaluate_script('editor.getValue()').should == new_gitignore_content + expect(evaluate_script('blob.editor.getValue()')).to eq new_gitignore_content end step 'I edit code' do @@ -61,8 +69,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps fill_in :file_name, with: new_file_name end + step 'I fill the new branch name' do + fill_in :new_branch, with: 'new_branch_name' + end + step 'I fill the new file name with an illegal name' do - fill_in :file_name, with: '.git' + fill_in :file_name, with: 'Spaces Not Allowed' end step 'I fill the commit message' do @@ -70,7 +82,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I click link "Diff"' do - click_link 'Diff' + click_link 'Preview changes' end step 'I click on "Commit Changes"' do @@ -86,7 +98,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I see diff' do - page.should have_css '.line_holder.new' + expect(page).to have_css '.line_holder.new' end step 'I click on "new file" link in repo' do @@ -94,9 +106,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I can see new file page' do - page.should have_content "New file" - page.should have_content "File name" - page.should have_content "Commit message" + expect(page).to have_content "New file" + expect(page).to have_content "Commit message" end step 'I click on files directory' do @@ -108,25 +119,25 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I see Browse dir link' do - page.should have_link 'Browse Dir »' - page.should_not have_link 'Browse Code »' + expect(page).to have_link 'Browse Dir »' + expect(page).not_to have_link 'Browse Code »' end step 'I click on readme file' do - within '.tree-table' do + page.within '.tree-table' do click_link 'README.md' end end step 'I see Browse file link' do - page.should have_link 'Browse File »' - page.should_not have_link 'Browse Code »' + expect(page).to have_link 'Browse File »' + expect(page).not_to have_link 'Browse Code »' end step 'I see Browse code link' do - page.should have_link 'Browse Code »' - page.should_not have_link 'Browse File »' - page.should_not have_link 'Browse Dir »' + expect(page).to have_link 'Browse Code »' + expect(page).not_to have_link 'Browse File »' + expect(page).not_to have_link 'Browse Dir »' end step 'I click on Permalink' do @@ -134,21 +145,33 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I am redirected to the files URL' do - current_path.should == project_tree_path(@project, 'master') + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, 'master') end step 'I am redirected to the ".gitignore"' do - expect(current_path).to eq(project_blob_path(@project, 'master/.gitignore')) + expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'master/.gitignore')) + end + + step 'I am redirected to the ".gitignore" on new branch' do + expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'new_branch_name/.gitignore')) end step 'I am redirected to the permalink URL' do - expect(current_path).to eq(project_blob_path( - @project, @project.repository.commit.sha + '/.gitignore')) + expect(current_path).to( + eq(namespace_project_blob_path(@project.namespace, @project, + @project.repository.commit.sha + + '/.gitignore')) + ) end step 'I am redirected to the new file' do - expect(current_path).to eq(project_blob_path( - @project, 'master/' + new_file_name)) + expect(current_path).to eq(namespace_project_blob_path( + @project.namespace, @project, 'master/' + new_file_name)) + end + + step 'I am redirected to the new file on new branch' do + expect(current_path).to eq(namespace_project_blob_path( + @project.namespace, @project, 'new_branch_name/' + new_file_name)) end step "I don't see the permalink link" do @@ -159,10 +182,21 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).to have_content('Your changes could not be committed') end + step 'I create bare repo' do + click_link 'Create empty bare repository' + end + + step 'I click on "add a file" link' do + click_link 'add a file' + + # Remove pre-receive hook so we can push without auth + FileUtils.rm_f(File.join(@project.repository.path, 'hooks', 'pre-receive')) + end + private def set_new_content - execute_script("editor.setValue('#{new_gitignore_content}')") + execute_script("blob.editor.setValue('#{new_gitignore_content}')") end # Content of the gitignore file on the seed repository. diff --git a/features/steps/project/source/git_blame.rb b/features/steps/project/source/git_blame.rb index e29a816c51b..d0a27f47e2a 100644 --- a/features/steps/project/source/git_blame.rb +++ b/features/steps/project/source/git_blame.rb @@ -12,8 +12,8 @@ class Spinach::Features::ProjectSourceGitBlame < Spinach::FeatureSteps end step 'I should see git file blame' do - page.should have_content "*.rb" - page.should have_content "Dmitriy Zaporozhets" - page.should have_content "Initial commit" + expect(page).to have_content "*.rb" + expect(page).to have_content "Dmitriy Zaporozhets" + expect(page).to have_content "Initial commit" end end diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index 53578ee5970..c78e86fa1a7 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -13,19 +13,19 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see files from repository in markdown' do - current_path.should == project_tree_path(@project, "markdown") - page.should have_content "README.md" - page.should have_content "CHANGELOG" + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "markdown") + expect(page).to have_content "README.md" + expect(page).to have_content "CHANGELOG" end step 'I should see rendered README which contains correct links' do - page.should have_content "Welcome to GitLab GitLab is a free project and repository management application" - page.should have_link "GitLab API doc" - page.should have_link "GitLab API website" - page.should have_link "Rake tasks" - page.should have_link "backup and restore procedure" - page.should have_link "GitLab API doc directory" - page.should have_link "Maintenance" + expect(page).to have_content "Welcome to GitLab GitLab is a free project and repository management application" + expect(page).to have_link "GitLab API doc" + expect(page).to have_link "GitLab API website" + expect(page).to have_link "Rake tasks" + expect(page).to have_link "backup and restore procedure" + expect(page).to have_link "GitLab API doc directory" + expect(page).to have_link "Maintenance" end step 'I click on Gitlab API in README' do @@ -33,8 +33,8 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see correct document rendered' do - current_path.should == project_blob_path(@project, "markdown/doc/api/README.md") - page.should have_content "All API requests require authentication" + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") + expect(page).to have_content "All API requests require authentication" end step 'I click on Rake tasks in README' do @@ -42,9 +42,9 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see correct directory rendered' do - current_path.should == project_tree_path(@project, "markdown/doc/raketasks") - page.should have_content "backup_restore.md" - page.should have_content "maintenance.md" + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "markdown/doc/raketasks") + expect(page).to have_content "backup_restore.md" + expect(page).to have_content "maintenance.md" end step 'I click on GitLab API doc directory in README' do @@ -52,9 +52,9 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see correct doc/api directory rendered' do - current_path.should == project_tree_path(@project, "markdown/doc/api") - page.should have_content "README.md" - page.should have_content "users.md" + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "markdown/doc/api") + expect(page).to have_content "README.md" + expect(page).to have_content "users.md" end step 'I click on Maintenance in README' do @@ -62,41 +62,41 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see correct maintenance file rendered' do - current_path.should == project_blob_path(@project, "markdown/doc/raketasks/maintenance.md") - page.should have_content "bundle exec rake gitlab:env:info RAILS_ENV=production" + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/raketasks/maintenance.md") + expect(page).to have_content "bundle exec rake gitlab:env:info RAILS_ENV=production" end step 'I click on link "empty" in the README' do - within('.readme-holder') do + page.within('.readme-holder') do click_link "empty" end end step 'I click on link "id" in the README' do - within('.readme-holder') do + page.within('.readme-holder') do click_link "#id" end end step 'I navigate to the doc/api/README' do - within '.tree-table' do + page.within '.tree-table' do click_link "doc" end - within '.tree-table' do + page.within '.tree-table' do click_link "api" end - within '.tree-table' do + page.within '.tree-table' do click_link "README.md" end end step 'I see correct file rendered' do - current_path.should == project_blob_path(@project, "markdown/doc/api/README.md") - page.should have_content "Contents" - page.should have_link "Users" - page.should have_link "Rake tasks" + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") + expect(page).to have_content "Contents" + expect(page).to have_link "Users" + expect(page).to have_link "Rake tasks" end step 'I click on users in doc/api/README' do @@ -104,8 +104,8 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see the correct document file' do - current_path.should == project_blob_path(@project, "markdown/doc/api/users.md") - page.should have_content "Get a list of users." + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") + expect(page).to have_content "Get a list of users." end step 'I click on raketasks in doc/api/README' do @@ -115,100 +115,100 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps # Markdown branch When 'I visit markdown branch' do - visit project_tree_path(@project, "markdown") + visit namespace_project_tree_path(@project.namespace, @project, "markdown") end When 'I visit markdown branch "README.md" blob' do - visit project_blob_path(@project, "markdown/README.md") + visit namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") end When 'I visit markdown branch "d" tree' do - visit project_tree_path(@project, "markdown/d") + visit namespace_project_tree_path(@project.namespace, @project, "markdown/d") end When 'I visit markdown branch "d/README.md" blob' do - visit project_blob_path(@project, "markdown/d/README.md") + visit namespace_project_blob_path(@project.namespace, @project, "markdown/d/README.md") end step 'I should see files from repository in markdown branch' do - current_path.should == project_tree_path(@project, "markdown") - page.should have_content "README.md" - page.should have_content "CHANGELOG" + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "markdown") + expect(page).to have_content "README.md" + expect(page).to have_content "CHANGELOG" end step 'I see correct file rendered in markdown branch' do - current_path.should == project_blob_path(@project, "markdown/doc/api/README.md") - page.should have_content "Contents" - page.should have_link "Users" - page.should have_link "Rake tasks" + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") + expect(page).to have_content "Contents" + expect(page).to have_link "Users" + expect(page).to have_link "Rake tasks" end step 'I should see correct document rendered for markdown branch' do - current_path.should == project_blob_path(@project, "markdown/doc/api/README.md") - page.should have_content "All API requests require authentication" + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") + expect(page).to have_content "All API requests require authentication" end step 'I should see correct directory rendered for markdown branch' do - current_path.should == project_tree_path(@project, "markdown/doc/raketasks") - page.should have_content "backup_restore.md" - page.should have_content "maintenance.md" + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "markdown/doc/raketasks") + expect(page).to have_content "backup_restore.md" + expect(page).to have_content "maintenance.md" end step 'I should see the users document file in markdown branch' do - current_path.should == project_blob_path(@project, "markdown/doc/api/users.md") - page.should have_content "Get a list of users." + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") + expect(page).to have_content "Get a list of users." end # Expected link contents step 'The link with text "empty" should have url "tree/markdown"' do - find('a', text: /^empty$/)['href'] == current_host + project_tree_path(@project, "markdown") + find('a', text: /^empty$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown") end step 'The link with text "empty" should have url "blob/markdown/README.md"' do - find('a', text: /^empty$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + find('a', text: /^empty$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") end step 'The link with text "empty" should have url "tree/markdown/d"' do - find('a', text: /^empty$/)['href'] == current_host + project_tree_path(@project, "markdown/d") + find('a', text: /^empty$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown/d") end step 'The link with text "empty" should have '\ 'url "blob/markdown/d/README.md"' do - find('a', text: /^empty$/)['href'] == current_host + project_blob_path(@project, "markdown/d/README.md") + find('a', text: /^empty$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/d/README.md") end step 'The link with text "ID" should have url "tree/markdownID"' do - find('a', text: /^#id$/)['href'] == current_host + project_tree_path(@project, "markdown") + '#id' + find('a', text: /^#id$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown") + '#id' end step 'The link with text "/ID" should have url "tree/markdownID"' do - find('a', text: /^\/#id$/)['href'] == current_host + project_tree_path(@project, "markdown") + '#id' + find('a', text: /^\/#id$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown") + '#id' end step 'The link with text "README.mdID" '\ 'should have url "blob/markdown/README.mdID"' do - find('a', text: /^README.md#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id' + find('a', text: /^README.md#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") + '#id' end step 'The link with text "d/README.mdID" should have '\ 'url "blob/markdown/d/README.mdID"' do - find('a', text: /^d\/README.md#id$/)['href'] == current_host + project_blob_path(@project, "d/markdown/README.md") + '#id' + find('a', text: /^d\/README.md#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "d/markdown/README.md") + '#id' end step 'The link with text "ID" should have url "blob/markdown/README.mdID"' do - find('a', text: /^#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id' + find('a', text: /^#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") + '#id' end step 'The link with text "/ID" should have url "blob/markdown/README.mdID"' do - find('a', text: /^\/#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id' + find('a', text: /^\/#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") + '#id' end # Wiki step 'I go to wiki page' do click_link "Wiki" - current_path.should == project_wiki_path(@project, "home") + expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "home") end step 'I add various links to the wiki page' do @@ -218,8 +218,8 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'Wiki page should have added links' do - current_path.should == project_wiki_path(@project, "home") - page.should have_content "test GitLab API doc Rake tasks" + expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "home") + expect(page).to have_content "test GitLab API doc Rake tasks" end step 'I add a header to the wiki page' do @@ -237,13 +237,13 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I see new wiki page named test' do - current_path.should == project_wiki_path(@project, "test") - page.should have_content "Editing" + expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "test") + expect(page).to have_content "Editing" end When 'I go back to wiki page home' do - visit project_wiki_path(@project, "home") - current_path.should == project_wiki_path(@project, "home") + visit namespace_project_wiki_path(@project.namespace, @project, "home") + expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "home") end step 'I click on GitLab API doc link' do @@ -251,8 +251,8 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I see Gitlab API document' do - current_path.should == project_wiki_path(@project, "api") - page.should have_content "Editing" + expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "api") + expect(page).to have_content "Editing" end step 'I click on Rake tasks link' do @@ -260,13 +260,13 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I see Rake tasks directory' do - current_path.should == project_wiki_path(@project, "raketasks") - page.should have_content "Editing" + expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "raketasks") + expect(page).to have_content "Editing" end step 'I go directory which contains README file' do - visit project_tree_path(@project, "markdown/doc/api") - current_path.should == project_tree_path(@project, "markdown/doc/api") + visit namespace_project_tree_path(@project.namespace, @project, "markdown/doc/api") + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "markdown/doc/api") end step 'I click on a relative link in README' do @@ -274,8 +274,8 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see the correct markdown' do - current_path.should == project_blob_path(@project, "markdown/doc/api/users.md") - page.should have_content "List users" + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") + expect(page).to have_content "List users" end step 'Header "Application details" should have correct id and link' do diff --git a/features/steps/project/source/multiselect_blob.rb b/features/steps/project/source/multiselect_blob.rb deleted file mode 100644 index b749ba49371..00000000000 --- a/features/steps/project/source/multiselect_blob.rb +++ /dev/null @@ -1,58 +0,0 @@ -class Spinach::Features::ProjectSourceMultiselectBlob < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - class << self - def click_line_steps(*line_numbers) - line_numbers.each do |line_number| - step "I click line #{line_number} in file" do - find("#L#{line_number}").click - end - - step "I shift-click line #{line_number} in file" do - script = "$('#L#{line_number}').trigger($.Event('click', { shiftKey: true }));" - execute_script(script) - end - end - end - - def check_state_steps(*ranges) - ranges.each do |range| - fragment = range.kind_of?(Array) ? "L#{range.first}-#{range.last}" : "L#{range}" - pluralization = range.kind_of?(Array) ? "s" : "" - - step "I should see \"#{fragment}\" as URI fragment" do - URI.parse(current_url).fragment.should == fragment - end - - step "I should see line#{pluralization} #{fragment[1..-1]} highlighted" do - ids = Array(range).map { |n| "LC#{n}" } - extra = false - - highlighted = all("#tree-content-holder .highlight .line.hll") - highlighted.each do |element| - extra ||= ids.delete(element[:id]).nil? - end - - extra.should be_false and ids.should be_empty - end - end - end - end - - click_line_steps *Array(1..5) - check_state_steps *Array(1..5), Array(1..2), Array(1..3), Array(1..4), Array(1..5), Array(3..5) - - step 'I go back in history' do - go_back - end - - step 'I go forward in history' do - go_forward - end - - step 'I click on ".gitignore" file in repo' do - click_link ".gitignore" - end -end diff --git a/features/steps/project/source/search_code.rb b/features/steps/project/source/search_code.rb index 9c2864cc936..feee756d7ec 100644 --- a/features/steps/project/source/search_code.rb +++ b/features/steps/project/source/search_code.rb @@ -9,11 +9,11 @@ class Spinach::Features::ProjectSourceSearchCode < Spinach::FeatureSteps end step 'I should see files from repository containing "coffee"' do - page.should have_content 'coffee' - page.should have_content 'CONTRIBUTING.md' + expect(page).to have_content 'coffee' + expect(page).to have_content 'CONTRIBUTING.md' end step 'I should see empty result' do - page.should have_content "We couldn't find any matching" + expect(page).to have_content "We couldn't find any" end end diff --git a/features/steps/project/star.rb b/features/steps/project/star.rb index ae2e4c7a201..8b50bfcef04 100644 --- a/features/steps/project/star.rb +++ b/features/steps/project/star.rb @@ -5,7 +5,7 @@ class Spinach::Features::ProjectStar < Spinach::FeatureSteps include SharedUser step "The project has no stars" do - page.should_not have_content '.star-buttons' + expect(page).not_to have_content '.star-buttons' end step "The project has 0 stars" do @@ -22,12 +22,16 @@ class Spinach::Features::ProjectStar < Spinach::FeatureSteps # Requires @javascript step "I click on the star toggle button" do - find(".star .toggle", visible: true).click + find(".star-btn", visible: true).click + end + + step 'I redirected to sign in page' do + expect(current_path).to eq new_user_session_path end protected def has_n_stars(n) - expect(page).to have_css(".star .count", text: /^#{n}$/, visible: true) + expect(page).to have_css(".star-btn .count", text: n, visible: true) end end diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index 7907f2a6fe3..97d63016458 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -5,52 +5,72 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps include Select2Helper step 'I should be able to see myself in team' do - page.should have_content(@user.name) - page.should have_content(@user.username) + expect(page).to have_content(@user.name) + expect(page).to have_content(@user.username) end - step 'I should see "Sam" in team list' do - user = User.find_by(name: "Sam") - page.should have_content(user.name) - page.should have_content(user.username) + step 'I should see "Dmitriy" in team list' do + user = User.find_by(name: "Dmitriy") + expect(page).to have_content(user.name) + expect(page).to have_content(user.username) end - step 'I click link "New Team Member"' do - click_link "New project member" + step 'I click link "Add members"' do + find(:css, 'button.btn-new').click end step 'I select "Mike" as "Reporter"' do user = User.find_by(name: "Mike") - select2(user.id, from: "#user_ids", multiple: true) - within "#new_project_member" do + page.within ".users-project-form" do + select2(user.id, from: "#user_ids", multiple: true) select "Reporter", from: "access_level" end - click_button "Add users" + click_button "Add users to project" end step 'I should see "Mike" in team list as "Reporter"' do - within ".access-reporter" do - page.should have_content('Mike') + page.within ".access-reporter" do + expect(page).to have_content('Mike') end end - step 'I should see "Sam" in team list as "Developer"' do - within ".access-developer" do - page.should have_content('Sam') + step 'I select "sjobs@apple.com" as "Reporter"' do + page.within ".users-project-form" do + select2("sjobs@apple.com", from: "#user_ids", multiple: true) + select "Reporter", from: "access_level" end + click_button "Add users to project" end - step 'I change "Sam" role to "Reporter"' do - user = User.find_by(name: "Sam") - within "#user_#{user.id}" do + step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do + page.within ".access-reporter" do + expect(page).to have_content('sjobs@apple.com') + expect(page).to have_content('invited') + expect(page).to have_content('Reporter') + end + end + + step 'I should see "Dmitriy" in team list as "Developer"' do + page.within ".access-developer" do + expect(page).to have_content('Dmitriy') + end + end + + step 'I change "Dmitriy" role to "Reporter"' do + project = Project.find_by(name: "Shop") + user = User.find_by(name: 'Dmitriy') + project_member = project.project_members.find_by(user_id: user.id) + page.within "#project_member_#{project_member.id}" do + click_button "Edit access level" select "Reporter", from: "project_member_access_level" + click_button "Save" end end - step 'I should see "Sam" in team list as "Reporter"' do - within ".access-reporter" do - page.should have_content('Sam') + step 'I should see "Dmitriy" in team list as "Reporter"' do + page.within ".access-reporter" do + expect(page).to have_content('Dmitriy') end end @@ -58,22 +78,22 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps click_link "Remove from team" end - step 'I should not see "Sam" in team list' do - user = User.find_by(name: "Sam") - page.should_not have_content(user.name) - page.should_not have_content(user.username) + step 'I should not see "Dmitriy" in team list' do + user = User.find_by(name: "Dmitriy") + expect(page).not_to have_content(user.name) + expect(page).not_to have_content(user.username) end step 'gitlab user "Mike"' do create(:user, name: "Mike") end - step 'gitlab user "Sam"' do - create(:user, name: "Sam") + step 'gitlab user "Dmitriy"' do + create(:user, name: "Dmitriy") end - step '"Sam" is "Shop" developer' do - user = User.find_by(name: "Sam") + step '"Dmitriy" is "Shop" developer' do + user = User.find_by(name: "Dmitriy") project = Project.find_by(name: "Shop") project.team << [user, :developer] end @@ -99,8 +119,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps click_button 'Import' end - step 'I click cancel link for "Sam"' do - within "#user_#{User.find_by(name: 'Sam').id}" do + step 'I click cancel link for "Dmitriy"' do + project = Project.find_by(name: "Shop") + user = User.find_by(name: 'Dmitriy') + project_member = project.project_members.find_by(user_id: user.id) + page.within "#project_member_#{project_member.id}" do click_link('Remove user from team') end end diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index aa00818c602..eebfaee1ede 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -3,15 +3,16 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps include SharedProject include SharedNote include SharedPaths + include WikiHelper step 'I click on the Cancel button' do - within(:css, ".form-actions") do + page.within(:css, ".form-actions") do click_on "Cancel" end end step 'I should be redirected back to the Edit Home Wiki page' do - current_path.should == project_wiki_path(project, :home) + expect(current_path).to eq namespace_project_wiki_path(project.namespace, project, :home) end step 'I create the Wiki Home page' do @@ -20,11 +21,11 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see the newly created wiki page' do - page.should have_content "Home" - page.should have_content "link test" + expect(page).to have_content "Home" + expect(page).to have_content "link test" click_link "link test" - page.should have_content "Editing" + expect(page).to have_content "Editing" end step 'I have an existing Wiki page' do @@ -33,7 +34,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I browse to that Wiki page' do - visit project_wiki_path(project, @page) + visit namespace_project_wiki_path(project.namespace, project, @page) end step 'I click on the Edit button' do @@ -46,11 +47,11 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see the updated content' do - page.should have_content "Updated Wiki Content" + expect(page).to have_content "Updated Wiki Content" end step 'I should be redirected back to that Wiki page' do - current_path.should == project_wiki_path(project, @page) + expect(current_path).to eq namespace_project_wiki_path(project.namespace, project, @page) end step 'That page has two revisions' do @@ -62,9 +63,9 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see both revisions' do - page.should have_content current_user.name - page.should have_content "first commit" - page.should have_content "second commit" + expect(page).to have_content current_user.name + expect(page).to have_content "first commit" + expect(page).to have_content "second commit" end step 'I click on the "Delete this page" button' do @@ -72,7 +73,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'The page should be deleted' do - page.should have_content "Page was successfully deleted" + expect(page).to have_content "Page was successfully deleted" end step 'I click on the "Pages" button' do @@ -80,8 +81,8 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see the existing page in the pages list' do - page.should have_content current_user.name - page.should have_content @page.title + expect(page).to have_content current_user.name + expect(page).to have_content @page.title end step 'I have an existing Wiki page with images linked on page' do @@ -90,37 +91,87 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I browse to wiki page with images' do - visit project_wiki_path(project, @wiki_page) + visit namespace_project_wiki_path(project.namespace, project, @wiki_page) end step 'I click on existing image link' do 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") - page.should have_link('image', href: "image.jpg") + expect(page).to have_link('image', href: "image.jpg") click_on "image" end step 'I should see the image from wiki repo' do - current_path.should match('wikis/image.jpg') - page.should_not have_xpath('/html') # Page should render the image which means there is no html involved + expect(current_path).to match('wikis/image.jpg') + expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved Gollum::Wiki.any_instance.unstub(:file) Gollum::File.any_instance.unstub(:mime_type) end step 'Image should be shown on the page' do - page.should have_xpath("//img[@src=\"image.jpg\"]") + expect(page).to have_xpath("//img[@src=\"image.jpg\"]") end step 'I click on image link' do - page.should have_link('image', href: "image.jpg") + expect(page).to have_link('image', href: "image.jpg") click_on "image" end step 'I should see the new wiki page form' do - current_path.should match('wikis/image.jpg') - page.should have_content('New Wiki Page') - page.should have_content('Editing - image.jpg') + expect(current_path).to match('wikis/image.jpg') + expect(page).to have_content('New Wiki Page') + expect(page).to have_content('Editing - image.jpg') + end + + step 'I create a New page with paths' do + click_on 'New Page' + fill_in 'Page slug', with: 'one/two/three' + click_on 'Build' + fill_in "wiki_content", with: 'wiki content' + click_on "Create page" + expect(current_path).to include 'one/two/three' + end + + step 'I create a New page with an invalid name' do + click_on 'New Page' + fill_in 'Page slug', with: 'invalid name' + click_on 'Build' + end + + step 'I should see an error message' do + expect(page).to have_content "The page slug is invalid" + end + + step 'I should see non-escaped link in the pages list' do + expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']") + end + + step 'I edit the Wiki page with a path' do + click_on 'three' + click_on 'Edit' + end + + step 'I should see a non-escaped path' do + expect(current_path).to include 'one/two/three' + end + + step 'I should see the Editing page' do + expect(page).to have_content('Editing') + end + + step 'I view the page history of a Wiki page that has a path' do + click_on 'three' + click_on 'Page History' + end + + step 'I should see the page history' do + expect(page).to have_content('History for') + end + + step 'I search for Wiki content' do + fill_in "Search in this project", with: "wiki_content" + click_button "Search" end def wiki diff --git a/features/steps/search.rb b/features/steps/search.rb index f3d8bd80f13..87893aa0205 100644 --- a/features/steps/search.rb +++ b/features/steps/search.rb @@ -18,30 +18,43 @@ class Spinach::Features::Search < Spinach::FeatureSteps click_button "Search" end + step 'I search for "Wiki content"' do + fill_in "dashboard_search", with: "content" + click_button "Search" + end + step 'I click "Issues" link' do - within '.search-filter' do + page.within '.search-filter' do click_link 'Issues' end end step 'I click project "Shop" link' do - within '.project-filter' do + page.within '.project-filter' do click_link project.name_with_namespace end end step 'I click "Merge requests" link' do - within '.search-filter' do + page.within '.search-filter' do click_link 'Merge requests' end end + step 'I click "Wiki" link' do + page.within '.search-filter' do + click_link 'Wiki' + end + end + step 'I should see "Shop" project link' do - page.should have_link "Shop" + expect(page).to have_link "Shop" end step 'I should see code results for project "Shop"' do - page.should have_content 'Update capybara, rspec-rails, poltergeist to recent versions' + page.within('.results') do + page.should have_content 'Update capybara, rspec-rails, poltergeist to recent versions' + end end step 'I search for "Contibuting"' do @@ -59,11 +72,24 @@ class Spinach::Features::Search < Spinach::FeatureSteps create(:merge_request, :simple, title: "Bar", source_project: project, target_project: project) end - step 'I should see "Foo" link' do - page.should have_link "Foo" + step 'I should see "Foo" link in the search results' do + page.within('.results') do + find(:css, '.search-results').should have_link 'Foo' + end + end + + step 'I should not see "Bar" link in the search results' do + expect(find(:css, '.search-results')).not_to have_link 'Bar' + end + + step 'I should see "test_wiki" link in the search results' do + page.within('.results') do + find(:css, '.search-results').should have_link 'test_wiki.md' + end end - step 'I should not see "Bar" link' do - page.should_not have_link "Bar" + step 'project has Wiki content' do + @wiki = ::ProjectWiki.new(project, current_user) + @wiki.create_page("test_wiki", "Some Wiki content", :markdown, "first commit") end end diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb index f41b59a6f2b..72d873caa57 100644 --- a/features/steps/shared/active_tab.rb +++ b/features/steps/shared/active_tab.rb @@ -2,31 +2,31 @@ module SharedActiveTab include Spinach::DSL def ensure_active_main_tab(content) - find('.main-nav li.active').should have_content(content) + expect(find('.nav-sidebar > li.active')).to have_content(content) end def ensure_active_sub_tab(content) - find('div.content ul.nav-tabs li.active').should have_content(content) + expect(find('div.content ul.nav-tabs li.active')).to have_content(content) end def ensure_active_sub_nav(content) - find('div.content ul.nav-stacked-menu li.active').should have_content(content) + expect(find('.sidebar-subnav > li.active')).to have_content(content) end step 'no other main tabs should be active' do - page.should have_selector('.main-nav li.active', count: 1) + expect(page).to have_selector('.nav-sidebar > li.active', count: 1) end step 'no other sub tabs should be active' do - page.should have_selector('div.content ul.nav-tabs li.active', count: 1) + expect(page).to have_selector('div.content ul.nav-tabs li.active', count: 1) end step 'no other sub navs should be active' do - page.should have_selector('div.content ul.nav-stacked-menu li.active', count: 1) + 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('Activity') + ensure_active_main_tab('Your Projects') end step 'the active main tab should be Projects' do diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb index ac8a3df6bb9..735e0ef6108 100644 --- a/features/steps/shared/authentication.rb +++ b/features/steps/shared/authentication.rb @@ -21,13 +21,17 @@ module SharedAuthentication end step 'I should be redirected to sign in page' do - current_path.should == new_user_session_path + expect(current_path).to eq new_user_session_path end step "I logout" do logout end + step "I logout directly" do + logout_direct + end + def current_user @user || User.first end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 10f3ed90b56..27a95aeb19a 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -3,7 +3,7 @@ module SharedDiffNote include RepoHelpers step 'I cancel the diff comment' do - within(diff_file_selector) do + page.within(diff_file_selector) do find(".js-close-discussion-note-form").click end end @@ -14,152 +14,206 @@ module SharedDiffNote end step 'I haven\'t written any diff comment text' do - within(diff_file_selector) do + page.within(diff_file_selector) do fill_in "note[note]", with: "" end end step 'I leave a diff comment like "Typo, please fix"' do - click_diff_line(sample_commit.line_code) - within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do - fill_in "note[note]", with: "Typo, please fix" + page.within(diff_file_selector) do + click_diff_line(sample_commit.line_code) + + page.within("form[rel$='#{sample_commit.line_code}']") do + fill_in "note[note]", with: "Typo, please fix" + find(".js-comment-button").trigger("click") + sleep 0.05 + end + end + end + + 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[rel$='#{sample_commit.line_code}']") do + fill_in "note[note]", with: "Old comment" + find(".js-comment-button").trigger("click") + end + end + + 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[rel$='#{sample_commit.line_code}']") do + fill_in "note[note]", with: "New comment" find(".js-comment-button").trigger("click") - sleep 0.05 end end step 'I preview a diff comment text like "Should fix it :smile:"' do - click_diff_line(sample_commit.line_code) - within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do - fill_in "note[note]", with: "Should fix it :smile:" - find(".js-note-preview-button").trigger("click") + page.within(diff_file_selector) do + click_diff_line(sample_commit.line_code) + + page.within("form[rel$='#{sample_commit.line_code}']") do + fill_in "note[note]", with: "Should fix it :smile:" + find('.js-md-preview-button').click + end end end step 'I preview another diff comment text like "DRY this up"' do - click_diff_line(sample_commit.del_line_code) + page.within(diff_file_selector) do + click_diff_line(sample_commit.del_line_code) - within("#{diff_file_selector} form[rel$='#{sample_commit.del_line_code}']") do - fill_in "note[note]", with: "DRY this up" - find(".js-note-preview-button").trigger("click") + page.within("form[rel$='#{sample_commit.del_line_code}']") do + fill_in "note[note]", with: "DRY this up" + find('.js-md-preview-button').click + end end end step 'I open a diff comment form' do - click_diff_line(sample_commit.line_code) + page.within(diff_file_selector) do + click_diff_line(sample_commit.line_code) + end end step 'I open another diff comment form' do - click_diff_line(sample_commit.del_line_code) + page.within(diff_file_selector) do + click_diff_line(sample_commit.del_line_code) + end end step 'I write a diff comment like ":-1: I don\'t like this"' do - within(diff_file_selector) do + page.within(diff_file_selector) do fill_in "note[note]", with: ":-1: I don\'t like this" end end step 'I submit the diff comment' do - within(diff_file_selector) do + page.within(diff_file_selector) do click_button("Add Comment") end end step 'I should not see the diff comment form' do - within(diff_file_selector) do - page.should_not have_css("form.new_note") + page.within(diff_file_selector) do + expect(page).not_to have_css("form.new_note") end end - step 'I should not see the diff comment preview button' do - within(diff_file_selector) do - page.should have_css(".js-note-preview-button", visible: false) + step 'The diff comment preview tab should say there is nothing to do' do + page.within(diff_file_selector) do + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_content('Nothing to preview.') end end step 'I should not see the diff comment text field' do - within(diff_file_selector) do - page.should have_css(".js-note-text", visible: false) + page.within(diff_file_selector) do + expect(find('.js-note-text')).not_to be_visible end end step 'I should only see one diff form' do - within(diff_file_selector) do - page.should have_css("form.new_note", count: 1) + page.within(diff_file_selector) do + expect(page).to have_css("form.new_note", count: 1) end end step 'I should see a diff comment form with ":-1: I don\'t like this"' do - within(diff_file_selector) do - page.should have_field("note[note]", with: ":-1: I don\'t like this") + page.within(diff_file_selector) do + expect(page).to have_field("note[note]", with: ":-1: I don\'t like this") end end step 'I should see a diff comment saying "Typo, please fix"' do - within("#{diff_file_selector} .note") do - page.should have_content("Typo, please fix") + page.within("#{diff_file_selector} .note") do + expect(page).to have_content("Typo, please fix") + end + end + + step 'I should see a diff comment on the left side saying "Old comment"' do + page.within("#{diff_file_selector} .notes_content.parallel.old") do + expect(page).to have_content("Old comment") + end + end + + step 'I should see a diff comment on the right side saying "New comment"' do + page.within("#{diff_file_selector} .notes_content.parallel.new") do + expect(page).to have_content("New comment") end end step 'I should see a discussion reply button' do - within(diff_file_selector) do - page.should have_button('Reply') + page.within(diff_file_selector) do + expect(page).to have_button('Reply') end end step 'I should see a temporary diff comment form' do - within(diff_file_selector) do - page.should have_css(".js-temp-notes-holder form.new_note") + page.within(diff_file_selector) do + expect(page).to have_css(".js-temp-notes-holder form.new_note") end end step 'I should see add a diff comment button' do - page.should have_css(".js-add-diff-note-button", visible: false) + expect(page).to have_css('.js-add-diff-note-button', visible: true) end step 'I should see an empty diff comment form' do - within(diff_file_selector) do - page.should have_field("note[note]", with: "") + page.within(diff_file_selector) do + expect(page).to have_field("note[note]", with: "") end end step 'I should see the cancel comment button' do - within("#{diff_file_selector} form") do - page.should have_css(".js-close-discussion-note-form", text: "Cancel") + page.within("#{diff_file_selector} form") do + expect(page).to have_css(".js-close-discussion-note-form", text: "Cancel") end end step 'I should see the diff comment preview' do - within("#{diff_file_selector} form") do - page.should have_css(".js-note-preview", visible: false) + page.within("#{diff_file_selector} form") do + expect(page).to have_css('.js-md-preview', visible: true) end end - step 'I should see the diff comment edit button' do - within(diff_file_selector) do - page.should have_css(".js-note-write-button", visible: true) + step 'I should see the diff comment write tab' do + page.within(diff_file_selector) do + expect(page).to have_css('.js-md-write-button', visible: true) end end - step 'I should see the diff comment preview button' do - within(diff_file_selector) do - page.should have_css(".js-note-preview-button", visible: true) + step 'The diff comment preview tab should display rendered Markdown' do + page.within(diff_file_selector) do + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_css('img.emoji', visible: true) end end step 'I should see two separate previews' do - within(diff_file_selector) do - page.should have_css(".js-note-preview", visible: true, count: 2) - page.should have_content("Should fix it") - page.should have_content("DRY this up") + page.within(diff_file_selector) do + expect(page).to have_css('.js-md-preview', visible: true, count: 2) + expect(page).to have_content('Should fix it') + expect(page).to have_content('DRY this up') end end + step 'I click side-by-side diff button' do + click_link "Side-by-side" + end + + step 'I see side-by-side diff button' do + expect(page).to have_content "Side-by-side" + end + def diff_file_selector - ".diff-file:nth-of-type(1)" + '.diff-file:nth-of-type(1)' end def click_diff_line(code) find("button[data-line-code='#{code}']").click end + + def click_parallel_diff_line(code, line_type) + find("button[data-line-code='#{code}'][data-line-type='#{line_type}']").trigger('click') + end end diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb index 1b225dd61a6..2d17fb34ccb 100644 --- a/features/steps/shared/group.rb +++ b/features/steps/shared/group.rb @@ -22,11 +22,11 @@ module SharedGroup end step 'I should see group "TestGroup"' do - page.should have_content "TestGroup" + expect(page).to have_content "TestGroup" end step 'I should not see group "TestGroup"' do - page.should_not have_content "TestGroup" + expect(page).not_to have_content "TestGroup" end protected diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb new file mode 100644 index 00000000000..41db2612f26 --- /dev/null +++ b/features/steps/shared/issuable.rb @@ -0,0 +1,15 @@ +module SharedIssuable + include Spinach::DSL + + def edit_issuable + find(:css, '.issuable-edit').click + end + + step 'I click link "Edit" for the merge request' do + edit_issuable + end + + step 'I click link "Edit" for the issue' do + edit_issuable + end +end diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb index 8bf138065b0..56b36f7c46c 100644 --- a/features/steps/shared/markdown.rb +++ b/features/steps/shared/markdown.rb @@ -2,56 +2,60 @@ module SharedMarkdown include Spinach::DSL def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki") - find(:css, "#{parent} h#{level}##{id}").text.should == text - find(:css, "#{parent} h#{level}##{id} > :last-child")[:href].should =~ /##{id}$/ + node = find("#{parent} h#{level} a##{id}") + expect(node[:href]).to eq "##{id}" + + # Work around a weird Capybara behavior where calling `parent` on a node + # returns the whole document, not the node's actual parent element + expect(find(:xpath, "#{node.path}/..").text).to eq text end - def create_taskable(type, title) - desc_text = <<EOT.gsub(/^ {6}/, '') - * [ ] Task 1 - * [x] Task 2 -EOT + step 'Header "Description header" should have correct id and link' do + header_should_have_correct_id_and_link(1, 'Description header', 'description-header') + end - case type - when :issue, :closed_issue - options = { project: project } - when :merge_request - options = { source_project: project, target_project: project } + step 'I should not see the Markdown preview' do + expect(find('.gfm-form .js-md-preview')).not_to be_visible + end + + step 'The Markdown preview tab should say there is nothing to do' do + page.within('.gfm-form') do + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_content('Nothing to preview.') end + end - create( - type, - options.merge(title: title, - author: project.users.first, - description: desc_text) - ) + step 'I should not see the Markdown text field' do + expect(find('.gfm-form textarea')).not_to be_visible end - step 'Header "Description header" should have correct id and link' do - header_should_have_correct_id_and_link(1, 'Description header', 'description-header') + step 'I should see the Markdown write tab' do + expect(find('.gfm-form')).to have_css('.js-md-write-button', visible: true) + end + + step 'I should see the Markdown preview' do + expect(find('.gfm-form')).to have_css('.js-md-preview', visible: true) end - step 'I should see task checkboxes in the description' do - expect(page).to have_selector( - 'div.description li.task-list-item input[type="checkbox"]' - ) + step 'The Markdown preview tab should display rendered Markdown' do + page.within('.gfm-form') do + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_css('img.emoji', visible: true) + end end - step 'I should see the task status for the Taskable' do - expect(find(:css, 'span.task-status').text).to eq( - '2 tasks (1 done, 1 unfinished)' - ) + step 'I write a description like ":+1: Nice"' do + find('.gfm-form').fill_in 'Description', with: ':+1: Nice' end - step 'Task checkboxes should be enabled' do - expect(page).to have_selector( - 'div.description li.task-list-item input[type="checkbox"]:enabled' - ) + step 'I preview a description text like "Bug fixed :smile:"' do + page.within('.gfm-form') do + fill_in 'Description', with: 'Bug fixed :smile:' + find('.js-md-preview-button').click + end end - step 'Task checkboxes should be disabled' do - expect(page).to have_selector( - 'div.description li.task-list-item input[type="checkbox"]:disabled' - ) + step 'I haven\'t written any description text' do + find('.gfm-form').fill_in 'Description', with: '' end end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 2b2cb47a715..f6aabfefeff 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -2,111 +2,114 @@ module SharedNote include Spinach::DSL step 'I delete a comment' do - find('.note').hover - find(".js-note-delete").click + page.within('.notes') do + find('.note').hover + find(".js-note-delete").click + end end step 'I haven\'t written any comment text' do - within(".js-main-target-form") do + page.within(".js-main-target-form") do fill_in "note[note]", with: "" end end step 'I leave a comment like "XML attached"' do - within(".js-main-target-form") do + page.within(".js-main-target-form") do fill_in "note[note]", with: "XML attached" click_button "Add Comment" - sleep 0.05 end end step 'I preview a comment text like "Bug fixed :smile:"' do - within(".js-main-target-form") do + page.within(".js-main-target-form") do fill_in "note[note]", with: "Bug fixed :smile:" - find(".js-note-preview-button").trigger("click") + find('.js-md-preview-button').click end end step 'I submit the comment' do - within(".js-main-target-form") do + page.within(".js-main-target-form") do click_button "Add Comment" end end - step 'I write a comment like "Nice"' do - within(".js-main-target-form") do - fill_in "note[note]", with: "Nice" + step 'I write a comment like ":+1: Nice"' do + page.within(".js-main-target-form") do + fill_in 'note[note]', with: ':+1: Nice' end end step 'I should not see a comment saying "XML attached"' do - page.should_not have_css(".note") + expect(page).not_to have_css(".note") end step 'I should not see the cancel comment button' do - within(".js-main-target-form") do + page.within(".js-main-target-form") do should_not have_link("Cancel") end end step 'I should not see the comment preview' do - within(".js-main-target-form") do - page.should have_css(".js-note-preview", visible: false) + page.within(".js-main-target-form") do + expect(find('.js-md-preview')).not_to be_visible end end - step 'I should not see the comment preview button' do - within(".js-main-target-form") do - page.should have_css(".js-note-preview-button", visible: false) + step 'The comment preview tab should say there is nothing to do' do + page.within(".js-main-target-form") do + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_content('Nothing to preview.') end end step 'I should not see the comment text field' do - within(".js-main-target-form") do - page.should have_css(".js-note-text", visible: false) + page.within(".js-main-target-form") do + expect(find('.js-note-text')).not_to be_visible end end step 'I should see a comment saying "XML attached"' do - within(".note") do - page.should have_content("XML attached") + page.within(".note") do + expect(page).to have_content("XML attached") end end step 'I should see an empty comment text field' do - within(".js-main-target-form") do - page.should have_field("note[note]", with: "") + page.within(".js-main-target-form") do + expect(page).to have_field("note[note]", with: "") end end - step 'I should see the comment edit button' do - within(".js-main-target-form") do - page.should have_css(".js-note-write-button", visible: true) + step 'I should see the comment write tab' do + page.within(".js-main-target-form") do + expect(page).to have_css('.js-md-write-button', visible: true) end end - step 'I should see the comment preview' do - within(".js-main-target-form") do - page.should have_css(".js-note-preview", visible: true) + step 'The comment preview tab should be display rendered Markdown' do + page.within(".js-main-target-form") do + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_css('img.emoji', visible: true) end end - step 'I should see the comment preview button' do - within(".js-main-target-form") do - page.should have_css(".js-note-preview-button", visible: true) + step 'I should see the comment preview' do + page.within(".js-main-target-form") do + expect(page).to have_css('.js-md-preview', visible: true) end end step 'I should see comment "XML attached"' do - within(".note") do - page.should have_content("XML attached") + page.within(".note") do + expect(page).to have_content("XML attached") end end # Markdown step 'I leave a comment with a header containing "Comment with a header"' do - within(".js-main-target-form") do + page.within(".js-main-target-form") do fill_in "note[note]", with: "# Comment with a header" click_button "Add Comment" sleep 0.05 @@ -114,23 +117,27 @@ module SharedNote end step 'The comment with the header should not have an ID' do - within(".note-text") do - page.should have_content("Comment with a header") - page.should_not have_css("#comment-with-a-header") + page.within(".note-body > .note-text") do + expect(page).to have_content("Comment with a header") + expect(page).not_to have_css("#comment-with-a-header") end end - step 'I leave a comment with task markdown' do - within('.js-main-target-form') do - fill_in 'note[note]', with: '* [x] Task item' - click_button 'Add Comment' - sleep 0.05 + step 'I edit the last comment with a +1' do + page.within(".notes") do + find(".note").hover + find('.js-note-edit').click + end + + page.within(".current-note-edit-form") do + fill_in 'note[note]', with: '+1 Awesome!' + click_button 'Save Comment' end end - step 'I should not see task checkboxes in the comment' do - expect(page).not_to have_selector( - 'li.note div.timeline-content input[type="checkbox"]' - ) + step 'I should see +1 in the description' do + page.within(".note") do + expect(page).to have_content("+1 Awesome!") + end end end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 5f292255ce1..3bd0d60281c 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -1,6 +1,7 @@ module SharedPaths include Spinach::DSL include RepoHelpers + include DashboardHelper step 'I visit new project page' do visit new_project_path @@ -31,7 +32,7 @@ module SharedPaths end step 'I visit group "Owned" members page' do - visit members_group_path(Group.find_by(name:"Owned")) + visit group_group_members_path(Group.find_by(name:"Owned")) end step 'I visit group "Owned" settings page' do @@ -51,7 +52,7 @@ module SharedPaths end step 'I visit group "Guest" members page' do - visit members_group_path(Group.find_by(name:"Guest")) + visit group_group_members_path(Group.find_by(name:"Guest")) end step 'I visit group "Guest" settings page' do @@ -71,11 +72,11 @@ module SharedPaths end step 'I visit dashboard issues page' do - visit issues_dashboard_path + visit assigned_issues_dashboard_path end step 'I visit dashboard merge requests page' do - visit merge_requests_dashboard_path + visit assigned_mrs_dashboard_path end step 'I visit dashboard search page' do @@ -86,6 +87,18 @@ module SharedPaths visit help_path end + step 'I visit dashboard groups page' do + visit dashboard_groups_path + end + + step 'I should be redirected to the dashboard groups page' do + expect(current_path).to eq dashboard_groups_path + end + + step 'I visit dashboard starred projects page' do + visit starred_dashboard_projects_path + end + # ---------------------------------------- # Profile # ---------------------------------------- @@ -94,6 +107,10 @@ module SharedPaths visit profile_path end + step 'I visit profile applications page' do + visit applications_profile_path + end + step 'I visit profile password page' do visit edit_profile_password_path end @@ -106,22 +123,14 @@ module SharedPaths visit profile_keys_path end - step 'I visit profile design page' do - visit design_profile_path + step 'I visit profile preferences page' do + visit profile_preferences_path end step 'I visit profile history page' do visit history_profile_path end - step 'I visit profile groups page' do - visit profile_groups_path - end - - step 'I should be redirected to the profile groups page' do - current_path.should == profile_groups_path - end - # ---------------------------------------- # Admin # ---------------------------------------- @@ -131,7 +140,7 @@ module SharedPaths end step 'I visit admin projects page' do - visit admin_projects_path + visit admin_namespaces_projects_path end step 'I visit admin users page' do @@ -162,59 +171,76 @@ module SharedPaths visit admin_teams_path end + step 'I visit admin settings page' do + visit admin_application_settings_path + end + + step 'I visit applications page' do + visit admin_applications_path + end + # ---------------------------------------- # Generic Project # ---------------------------------------- step "I visit my project's home page" do - visit project_path(@project) + visit namespace_project_path(@project.namespace, @project) end step "I visit my project's settings page" do - visit edit_project_path(@project) + visit edit_namespace_project_path(@project.namespace, @project) end step "I visit my project's files page" do - visit project_tree_path(@project, root_ref) + visit namespace_project_tree_path(@project.namespace, @project, root_ref) + end + + step 'I visit a binary file in the repo' do + visit namespace_project_blob_path(@project.namespace, @project, File.join( + root_ref, 'files/images/logo-black.png')) end step "I visit my project's commits page" do - visit project_commits_path(@project, root_ref, {limit: 5}) + visit namespace_project_commits_path(@project.namespace, @project, root_ref, {limit: 5}) end step "I visit my project's commits page for a specific path" do - visit project_commits_path(@project, root_ref + "/app/models/project.rb", {limit: 5}) + visit namespace_project_commits_path(@project.namespace, @project, root_ref + "/app/models/project.rb", {limit: 5}) end step 'I visit my project\'s commits stats page' do - visit stats_project_repository_path(@project) + visit stats_namespace_project_repository_path(@project.namespace, @project) end step "I visit my project's network page" do # Stub Graph max_size to speed up test (10 commits vs. 650) Network::Graph.stub(max_count: 10) - visit project_network_path(@project, root_ref) + visit namespace_project_network_path(@project.namespace, @project, root_ref) end step "I visit my project's issues page" do - visit project_issues_path(@project) + visit namespace_project_issues_path(@project.namespace, @project) end step "I visit my project's merge requests page" do - visit project_merge_requests_path(@project) + visit namespace_project_merge_requests_path(@project.namespace, @project) + end + + step "I visit my project's members page" do + visit namespace_project_project_members_path(@project.namespace, @project) end step "I visit my project's wiki page" do - visit project_wiki_path(@project, :home) + visit namespace_project_wiki_path(@project.namespace, @project, :home) end step 'I visit project hooks page' do - visit project_hooks_path(@project) + visit namespace_project_hooks_path(@project.namespace, @project) end step 'I visit project deploy keys page' do - visit project_deploy_keys_path(@project) + visit namespace_project_deploy_keys_path(@project.namespace, @project) end # ---------------------------------------- @@ -222,153 +248,133 @@ module SharedPaths # ---------------------------------------- step 'I visit project "Shop" page' do - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I visit project "Forked Shop" merge requests page' do - visit project_merge_requests_path(@forked_project) + visit namespace_project_merge_requests_path(@forked_project.namespace, @forked_project) end step 'I visit edit project "Shop" page' do - visit edit_project_path(project) + visit edit_namespace_project_path(project.namespace, project) end step 'I visit project branches page' do - visit project_branches_path(@project) + visit namespace_project_branches_path(@project.namespace, @project) end step 'I visit project protected branches page' do - visit project_protected_branches_path(@project) + visit namespace_project_protected_branches_path(@project.namespace, @project) end step 'I visit compare refs page' do - visit project_compare_index_path(@project) + visit namespace_project_compare_index_path(@project.namespace, @project) end step 'I visit project commits page' do - visit project_commits_path(@project, root_ref, {limit: 5}) + visit namespace_project_commits_path(@project.namespace, @project, root_ref, {limit: 5}) end step 'I visit project commits page for stable branch' do - visit project_commits_path(@project, 'stable', {limit: 5}) + visit namespace_project_commits_path(@project.namespace, @project, 'stable', {limit: 5}) end step 'I visit project source page' do - visit project_tree_path(@project, root_ref) + visit namespace_project_tree_path(@project.namespace, @project, root_ref) end step 'I visit blob file from repo' do - visit project_blob_path(@project, File.join(sample_commit.id, sample_blob.path)) + visit namespace_project_blob_path(@project.namespace, @project, File.join(sample_commit.id, sample_blob.path)) end step 'I visit ".gitignore" file in repo' do - visit project_blob_path(@project, File.join(root_ref, '.gitignore')) + visit namespace_project_blob_path(@project.namespace, @project, File.join(root_ref, '.gitignore')) end step 'I am on the new file page' do - current_path.should eq(project_new_tree_path(@project, root_ref)) + expect(current_path).to eq(namespace_project_create_blob_path(@project.namespace, @project, root_ref)) end step 'I am on the ".gitignore" edit file page' do - current_path.should eq(project_edit_tree_path( - @project, File.join(root_ref, '.gitignore'))) + expect(current_path).to eq(namespace_project_edit_blob_path( + @project.namespace, @project, File.join(root_ref, '.gitignore'))) end step 'I visit project source page for "6d39438"' do - visit project_tree_path(@project, "6d39438") + visit namespace_project_tree_path(@project.namespace, @project, "6d39438") end step 'I visit project source page for' \ ' "6d394385cf567f80a8fd85055db1ab4c5295806f"' do - visit project_tree_path(@project, + visit namespace_project_tree_path(@project.namespace, @project, '6d394385cf567f80a8fd85055db1ab4c5295806f') end step 'I visit project tags page' do - visit project_tags_path(@project) + visit namespace_project_tags_path(@project.namespace, @project) end step 'I visit project commit page' do - visit project_commit_path(@project, sample_commit.id) + visit namespace_project_commit_path(@project.namespace, @project, sample_commit.id) end step 'I visit project "Shop" issues page' do - visit project_issues_path(project) + visit namespace_project_issues_path(project.namespace, project) end step 'I visit issue page "Release 0.4"' do issue = Issue.find_by(title: "Release 0.4") - visit project_issue_path(issue.project, issue) - end - - step 'I visit issue page "Tasks-open"' do - issue = Issue.find_by(title: 'Tasks-open') - visit project_issue_path(issue.project, issue) - end - - step 'I visit issue page "Tasks-closed"' do - issue = Issue.find_by(title: 'Tasks-closed') - visit project_issue_path(issue.project, issue) + visit namespace_project_issue_path(issue.project.namespace, issue.project, issue) end step 'I visit project "Shop" labels page' do project = Project.find_by(name: 'Shop') - visit project_labels_path(project) + visit namespace_project_labels_path(project.namespace, project) end step 'I visit project "Forum" labels page' do project = Project.find_by(name: 'Forum') - visit project_labels_path(project) + visit namespace_project_labels_path(project.namespace, project) end step 'I visit project "Shop" new label page' do project = Project.find_by(name: 'Shop') - visit new_project_label_path(project) + visit new_namespace_project_label_path(project.namespace, project) end step 'I visit project "Forum" new label page' do project = Project.find_by(name: 'Forum') - visit new_project_label_path(project) + visit new_namespace_project_label_path(project.namespace, project) end step 'I visit merge request page "Bug NS-04"' do mr = MergeRequest.find_by(title: "Bug NS-04") - visit project_merge_request_path(mr.target_project, mr) + visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) end step 'I visit merge request page "Bug NS-05"' do mr = MergeRequest.find_by(title: "Bug NS-05") - visit project_merge_request_path(mr.target_project, mr) - end - - step 'I visit merge request page "MR-task-open"' do - mr = MergeRequest.find_by(title: 'MR-task-open') - visit project_merge_request_path(mr.target_project, mr) - end - - step 'I visit merge request page "MR-task-closed"' do - mr = MergeRequest.find_by(title: 'MR-task-closed') - visit project_merge_request_path(mr.target_project, mr) + visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) end step 'I visit project "Shop" merge requests page' do - visit project_merge_requests_path(project) + visit namespace_project_merge_requests_path(project.namespace, project) end step 'I visit forked project "Shop" merge requests page' do - visit project_merge_requests_path(project) + visit namespace_project_merge_requests_path(project.namespace, project) end step 'I visit project "Shop" milestones page' do - visit project_milestones_path(project) + visit namespace_project_milestones_path(project.namespace, project) end step 'I visit project "Shop" team page' do - visit project_team_index_path(project) + visit namespace_project_project_members_path(project.namespace, project) end step 'I visit project wiki page' do - visit project_wiki_path(@project, :home) + visit namespace_project_wiki_path(@project.namespace, @project, :home) end # ---------------------------------------- @@ -377,17 +383,22 @@ module SharedPaths step 'I visit project "Community" page' do project = Project.find_by(name: "Community") - visit project_path(project) + visit namespace_project_path(project.namespace, project) + end + + step 'I visit project "Community" source page' do + project = Project.find_by(name: 'Community') + visit namespace_project_tree_path(project.namespace, project, root_ref) end step 'I visit project "Internal" page' do project = Project.find_by(name: "Internal") - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I visit project "Enterprise" page' do project = Project.find_by(name: "Enterprise") - visit project_path(project) + visit namespace_project_path(project.namespace, project) end # ---------------------------------------- @@ -396,7 +407,7 @@ module SharedPaths step "I visit empty project page" do project = Project.find_by(name: "Empty Public Project") - visit project_path(project) + visit namespace_project_path(project.namespace, project) end # ---------------------------------------- @@ -424,7 +435,7 @@ module SharedPaths # ---------------------------------------- step 'I visit project "Shop" snippets page' do - visit project_snippets_path(project) + visit namespace_project_snippets_path(project.namespace, project) end step 'I visit snippets page' do @@ -448,6 +459,6 @@ module SharedPaths # ---------------------------------------- step 'page status code should be 404' do - status_code.should == 404 + expect(status_code).to eq 404 end end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 0bd5653538c..9ee2e5dfbed 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -14,6 +14,24 @@ module SharedProject @project.team << [@user, :master] end + step 'I disable snippets in project' do + @project.snippets_enabled = false + @project.save + end + + step 'I disable issues and merge requests in project' do + @project.issues_enabled = false + @project.merge_requests_enabled = false + @project.save + end + + # Add another user to project "Shop" + step 'I add a user to project "Shop"' do + @project = Project.find_by(name: "Shop") + other_user = create(:user, name: 'Alpha') + @project.team << [other_user, :master] + end + # Create another specific project called "Forum" step 'I own project "Forum"' do @project = Project.find_by(name: "Forum") @@ -28,6 +46,11 @@ module SharedProject @project.team << [@user, :master] end + step 'I visit my empty project page' do + project = Project.find_by(name: 'Empty Project') + visit namespace_project_path(project.namespace, project) + end + step 'project "Shop" has push event' do @project = Project.find_by(name: "Shop") @@ -56,13 +79,13 @@ module SharedProject step 'I should see project "Shop" activity feed' do project = Project.find_by(name: "Shop") - page.should have_content "#{@user.name} pushed new branch fix at #{project.name_with_namespace}" + expect(page).to have_content "#{@user.name} pushed new branch fix at #{project.name_with_namespace}" end step 'I should see project settings' do - current_path.should == edit_project_path(@project) - page.should have_content("Project name") - page.should have_content("Features:") + 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:") end def current_project @@ -78,11 +101,11 @@ module SharedProject end step 'I should see project "Enterprise"' do - page.should have_content "Enterprise" + expect(page).to have_content "Enterprise" end step 'I should not see project "Enterprise"' do - page.should_not have_content "Enterprise" + expect(page).not_to have_content "Enterprise" end step 'internal project "Internal"' do @@ -90,11 +113,11 @@ module SharedProject end step 'I should see project "Internal"' do - page.should have_content "Internal" + expect(page).to have_content "Internal" end step 'I should not see project "Internal"' do - page.should_not have_content "Internal" + expect(page).not_to have_content "Internal" end step 'public project "Community"' do @@ -102,11 +125,11 @@ module SharedProject end step 'I should see project "Community"' do - page.should have_content "Community" + expect(page).to have_content "Community" end step 'I should not see project "Community"' do - page.should_not have_content "Community" + expect(page).not_to have_content "Community" end step '"John Doe" owns private project "Enterprise"' do diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index 6aa4f1b20df..3b94b7d8621 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -28,6 +28,10 @@ module SharedProjectTab ensure_active_main_tab('Issues') end + step 'the active main tab should be Members' do + ensure_active_main_tab('Members') + end + step 'the active main tab should be Merge Requests' do ensure_active_main_tab('Merge Requests') end @@ -41,6 +45,8 @@ module SharedProjectTab end step 'the active main tab should be Settings' do - ensure_active_main_tab('Settings') + page.within '.nav-sidebar' do + expect(page).to have_content('Back to project') + end end end diff --git a/features/steps/snippet_search.rb b/features/steps/snippet_search.rb index 669c7186c1b..cf999879579 100644 --- a/features/steps/snippet_search.rb +++ b/features/steps/snippet_search.rb @@ -18,39 +18,39 @@ class Spinach::Features::SnippetSearch < Spinach::FeatureSteps end step 'I should see "line seven" in results' do - page.should have_content 'line seven' + expect(page).to have_content 'line seven' end step 'I should see "line four" in results' do - page.should have_content 'line four' + expect(page).to have_content 'line four' end step 'I should see "line ten" in results' do - page.should have_content 'line ten' + expect(page).to have_content 'line ten' end step 'I should not see "line eleven" in results' do - page.should_not have_content 'line eleven' + expect(page).not_to have_content 'line eleven' end step 'I should not see "line three" in results' do - page.should_not have_content 'line three' + expect(page).not_to have_content 'line three' end step 'I should see "Personal snippet one" in results' do - page.should have_content 'Personal snippet one' + expect(page).to have_content 'Personal snippet one' end step 'I should see "Personal snippet private" in results' do - page.should have_content 'Personal snippet private' + expect(page).to have_content 'Personal snippet private' end step 'I should not see "Personal snippet one" in results' do - page.should_not have_content 'Personal snippet one' + expect(page).not_to have_content 'Personal snippet one' end step 'I should not see "Personal snippet private" in results' do - page.should_not have_content 'Personal snippet private' + expect(page).not_to have_content 'Personal snippet private' end end diff --git a/features/steps/snippets/discover.rb b/features/steps/snippets/discover.rb index 2667c1e3d44..76379d09d02 100644 --- a/features/steps/snippets/discover.rb +++ b/features/steps/snippets/discover.rb @@ -4,15 +4,15 @@ class Spinach::Features::SnippetsDiscover < Spinach::FeatureSteps include SharedSnippet step 'I should see "Personal snippet one" in snippets' do - page.should have_content "Personal snippet one" + expect(page).to have_content "Personal snippet one" end step 'I should see "Personal snippet internal" in snippets' do - page.should have_content "Personal snippet internal" + expect(page).to have_content "Personal snippet internal" end step 'I should not see "Personal snippet private" in snippets' do - page.should_not have_content "Personal snippet private" + expect(page).not_to have_content "Personal snippet private" end def snippet diff --git a/features/steps/snippets/public_snippets.rb b/features/steps/snippets/public_snippets.rb index 67669dc0a69..2ebdca5ed30 100644 --- a/features/steps/snippets/public_snippets.rb +++ b/features/steps/snippets/public_snippets.rb @@ -4,11 +4,11 @@ class Spinach::Features::PublicSnippets < Spinach::FeatureSteps include SharedSnippet step 'I should see snippet "Personal snippet one"' do - page.should have_no_xpath("//i[@class='public-snippet']") + expect(page).to have_no_xpath("//i[@class='public-snippet']") end step 'I should see raw snippet "Personal snippet one"' do - page.should have_text(snippet.content) + expect(page).to have_text(snippet.content) end step 'I visit snippet page "Personal snippet one"' do diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb index de936db85ee..426da2918ea 100644 --- a/features/steps/snippets/snippets.rb +++ b/features/steps/snippets/snippets.rb @@ -9,11 +9,11 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps end step 'I should not see "Personal snippet one" in snippets' do - page.should_not have_content "Personal snippet one" + expect(page).not_to have_content "Personal snippet one" end step 'I click link "Edit"' do - within ".file-title" do + page.within ".file-title" do click_link "Edit" end end @@ -25,15 +25,27 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps step 'I submit new snippet "Personal snippet three"' do fill_in "personal_snippet_title", :with => "Personal snippet three" fill_in "personal_snippet_file_name", :with => "my_snippet.rb" - within('.file-editor') do + page.within('.file-editor') do find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of snippet three' end click_button "Create snippet" end + step 'I submit new internal snippet' do + fill_in "personal_snippet_title", :with => "Internal personal snippet one" + fill_in "personal_snippet_file_name", :with => "my_snippet.rb" + choose 'personal_snippet_visibility_level_10' + + page.within('.file-editor') do + find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of internal snippet' + end + + click_button "Create snippet" + end + step 'I should see snippet "Personal snippet three"' do - page.should have_content "Personal snippet three" - page.should have_content "Content of snippet three" + expect(page).to have_content "Personal snippet three" + expect(page).to have_content "Content of snippet three" end step 'I submit new title "Personal snippet new title"' do @@ -42,7 +54,7 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps end step 'I should see "Personal snippet new title"' do - page.should have_content "Personal snippet new title" + expect(page).to have_content "Personal snippet new title" end step 'I uncheck "Private" checkbox' do @@ -51,14 +63,22 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps end step 'I should see "Personal snippet one" public' do - page.should have_no_xpath("//i[@class='public-snippet']") + expect(page).to have_no_xpath("//i[@class='public-snippet']") end step 'I visit snippet page "Personal snippet one"' do visit snippet_path(snippet) end + step 'I visit snippet page "Internal personal snippet one"' do + visit snippet_path(internal_snippet) + end + def snippet @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one") end + + def internal_snippet + @snippet ||= PersonalSnippet.find_by!(title: "Internal personal snippet one") + end end diff --git a/features/steps/snippets/user.rb b/features/steps/snippets/user.rb index 866f637ab6c..007fcb2893f 100644 --- a/features/steps/snippets/user.rb +++ b/features/steps/snippets/user.rb @@ -8,43 +8,43 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps end step 'I should see "Personal snippet one" in snippets' do - page.should have_content "Personal snippet one" + expect(page).to have_content "Personal snippet one" end step 'I should see "Personal snippet private" in snippets' do - page.should have_content "Personal snippet private" + expect(page).to have_content "Personal snippet private" end step 'I should see "Personal snippet internal" in snippets' do - page.should have_content "Personal snippet internal" + expect(page).to have_content "Personal snippet internal" end step 'I should not see "Personal snippet one" in snippets' do - page.should_not have_content "Personal snippet one" + expect(page).not_to have_content "Personal snippet one" end step 'I should not see "Personal snippet private" in snippets' do - page.should_not have_content "Personal snippet private" + expect(page).not_to have_content "Personal snippet private" end step 'I should not see "Personal snippet internal" in snippets' do - page.should_not have_content "Personal snippet internal" + expect(page).not_to have_content "Personal snippet internal" end step 'I click "Internal" filter' do - within('.nav-stacked') do + page.within('.nav-tabs') do click_link "Internal" end end step 'I click "Private" filter' do - within('.nav-stacked') do + page.within('.nav-tabs') do click_link "Private" end end step 'I click "Public" filter' do - within('.nav-stacked') do + page.within('.nav-tabs') do click_link "Public" end end diff --git a/features/steps/user.rb b/features/steps/user.rb index d6f05ecb2c7..3230234cb6d 100644 --- a/features/steps/user.rb +++ b/features/steps/user.rb @@ -7,4 +7,37 @@ class Spinach::Features::User < Spinach::FeatureSteps step 'I should see user "John Doe" page' do expect(title).to match(/^\s*John Doe/) end + + step '"John Doe" has contributions' do + user = User.find_by(name: 'John Doe') + project = contributed_project + + # Issue controbution + issue_params = { title: 'Bug in old browser' } + Issues::CreateService.new(project, user, issue_params).execute + + # Push code contribution + push_params = { + project: project, + action: Event::PUSHED, + author_id: user.id, + data: { commit_count: 3 } + } + + Event.create(push_params) + end + + step 'I should see contributed projects' do + page.within '.contributed-projects' do + expect(page).to have_content(@contributed_project.name) + end + end + + step 'I should see contributions calendar' do + expect(page).to have_css('.cal-heatmap-container') + end + + def contributed_project + @contributed_project ||= create(:project, :public) + end end diff --git a/features/support/capybara.rb b/features/support/capybara.rb new file mode 100644 index 00000000000..31dbf0feb2f --- /dev/null +++ b/features/support/capybara.rb @@ -0,0 +1,24 @@ +require 'spinach/capybara' +require 'capybara/poltergeist' + +# Give CI some extra time +timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 + +Capybara.javascript_driver = :poltergeist +Capybara.register_driver :poltergeist do |app| + Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout) +end + +Spinach.hooks.on_tag("javascript") do + Capybara.current_driver = Capybara.javascript_driver +end + +Capybara.default_wait_time = timeout +Capybara.ignore_hidden_elements = false + +unless ENV['CI'] || ENV['CI_SERVER'] + require 'capybara-screenshot/spinach' + + # Keep only the screenshots generated from the last failing test suite + Capybara::Screenshot.prune_strategy = :keep_last_run +end diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb new file mode 100644 index 00000000000..1ab308cfa55 --- /dev/null +++ b/features/support/db_cleaner.rb @@ -0,0 +1,11 @@ +require 'database_cleaner' + +DatabaseCleaner.strategy = :truncation + +Spinach.hooks.before_scenario do + DatabaseCleaner.start +end + +Spinach.hooks.after_scenario do + DatabaseCleaner.clean +end diff --git a/features/support/env.rb b/features/support/env.rb index 67660777842..672251af084 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -9,46 +9,24 @@ end ENV['RAILS_ENV'] = 'test' require './config/environment' -require 'rspec' require 'rspec/expectations' -require 'database_cleaner' -require 'spinach/capybara' require 'sidekiq/testing/inline' +require_relative 'capybara' +require_relative 'db_cleaner' + %w(select2_helper test_env repo_helpers).each do |f| require Rails.root.join('spec', 'support', f) end -Dir["#{Rails.root}/features/steps/shared/*.rb"].each {|file| require file} +Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file } WebMock.allow_net_connect! -# -# JS driver -# -require 'capybara/poltergeist' -Capybara.javascript_driver = :poltergeist -Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, js_errors: false, timeout: 90) -end -Spinach.hooks.on_tag("javascript") do - ::Capybara.current_driver = ::Capybara.javascript_driver -end -Capybara.default_wait_time = 60 -Capybara.ignore_hidden_elements = false - -DatabaseCleaner.strategy = :truncation - -Spinach.hooks.before_scenario do - DatabaseCleaner.start -end - -Spinach.hooks.after_scenario do - DatabaseCleaner.clean -end Spinach.hooks.before_run do + include RSpec::Mocks::ExampleMethods + RSpec::Mocks.setup TestEnv.init(mailer: false) - RSpec::Mocks::setup self include FactoryGirl::Syntax::Methods end diff --git a/features/user.feature b/features/user.feature index a2167935fd2..69618e929c4 100644 --- a/features/user.feature +++ b/features/user.feature @@ -67,3 +67,12 @@ Feature: User And I should see project "Enterprise" And I should not see project "Internal" And I should not see project "Community" + + @javascript + Scenario: "John Doe" contribution profile + Given I sign in as a user + And "John Doe" has contributions + When I visit user "John Doe" page + Then I should see user "John Doe" page + And I should see contributed projects + And I should see contributions calendar diff --git a/lib/api/api.rb b/lib/api/api.rb index d26667ba3f7..d2a35c78fc1 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -2,10 +2,11 @@ Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} module API class API < Grape::API + include APIGuard version 'v3', using: :path rescue_from ActiveRecord::RecordNotFound do - rack_response({'message' => '404 Not found'}.to_json, 404) + rack_response({ 'message' => '404 Not found' }.to_json, 404) end rescue_from :all do |exception| @@ -18,7 +19,7 @@ module API message << " " << trace.join("\n ") API.logger.add Logger::FATAL, message - rack_response({'message' => '500 Internal Server Error'}, 500) + rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500) end format :json diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb new file mode 100644 index 00000000000..b9994fcefda --- /dev/null +++ b/lib/api/api_guard.rb @@ -0,0 +1,172 @@ +# Guard API with OAuth 2.0 Access Token + +require 'rack/oauth2' + +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 + + # Must yield access token to store it in the env + request.access_token + end + + helpers HelperMethods + + 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) + 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) + + 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 + + def current_user + @current_user + end + + private + def find_access_token + @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) + end + + def doorkeeper_request + @doorkeeper_request ||= ActionDispatch::Request.new(env) + end + + def validate_access_token(access_token, scopes) + Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) + end + 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 + end + end + + private + def install_error_responders(base) + error_classes = [ MissingTokenError, TokenNotFoundError, + ExpiredError, RevokedError, InsufficientScopeError] + + 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 + + # + # Exceptions + # + + class MissingTokenError < StandardError; end + + class TokenNotFoundError < StandardError; end + + class ExpiredError < StandardError; end + + class RevokedError < StandardError; end + + class InsufficientScopeError < StandardError + attr_reader :scopes + def initialize(scopes) + @scopes = scopes + end + end +end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 6ec1a753a69..592100a7045 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -14,7 +14,8 @@ module API # Example Request: # GET /projects/:id/repository/branches get ":id/repository/branches" do - present user_project.repository.branches.sort_by(&:name), with: Entities::RepoObject, project: user_project + branches = user_project.repository.branches.sort_by(&:name) + present branches, with: Entities::RepoObject, project: user_project end # Get a single branch @@ -26,7 +27,7 @@ module API # GET /projects/:id/repository/branches/:branch get ':id/repository/branches/:branch', requirements: { branch: /.*/ } do @branch = user_project.repository.branches.find { |item| item.name == params[:branch] } - not_found!("Branch does not exist") if @branch.nil? + not_found!("Branch") unless @branch present @branch, with: Entities::RepoObject, project: user_project end @@ -43,7 +44,7 @@ module API authorize_admin_project @branch = user_project.repository.find_branch(params[:branch]) - not_found! unless @branch + not_found!("Branch") unless @branch protected_branch = user_project.protected_branches.find_by(name: @branch.name) user_project.protected_branches.create(name: @branch.name) unless protected_branch @@ -63,7 +64,7 @@ module API authorize_admin_project @branch = user_project.repository.find_branch(params[:branch]) - not_found! unless @branch + not_found!("Branch does not exist") unless @branch protected_branch = user_project.protected_branches.find_by(name: @branch.name) protected_branch.destroy if protected_branch @@ -99,7 +100,8 @@ module API # branch (required) - The name of the branch # Example Request: # DELETE /projects/:id/repository/branches/:branch - delete ":id/repository/branches/:branch" do + delete ":id/repository/branches/:branch", + requirements: { branch: /.*/ } do authorize_push_project result = DeleteBranchService.new(user_project, current_user). execute(params[:branch]) diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 6c5391b98c8..f4efb651eb6 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -32,7 +32,7 @@ module API # GET /projects/:id/repository/commits/:sha get ":id/repository/commits/:sha" do sha = params[:sha] - commit = user_project.repository.commit(sha) + commit = user_project.commit(sha) not_found! "Commit" unless commit present commit, with: Entities::RepoCommitDetail end @@ -46,7 +46,7 @@ module API # GET /projects/:id/repository/commits/:sha/diff get ":id/repository/commits/:sha/diff" do sha = params[:sha] - commit = user_project.repository.commit(sha) + commit = user_project.commit(sha) not_found! "Commit" unless commit commit.diffs end @@ -60,9 +60,9 @@ module API # GET /projects/:id/repository/commits/:sha/comments get ':id/repository/commits/:sha/comments' do sha = params[:sha] - commit = user_project.repository.commit(sha) + commit = user_project.commit(sha) not_found! 'Commit' unless commit - notes = Note.where(commit_id: commit.id) + notes = Note.where(commit_id: commit.id).order(:created_at) present paginate(notes), with: Entities::CommitNote end @@ -81,7 +81,7 @@ module API required_attributes! [:note] sha = params[:sha] - commit = user_project.repository.commit(sha) + commit = user_project.commit(sha) not_found! 'Commit' unless commit opts = { note: params[:note], @@ -108,7 +108,7 @@ module API if note.save present note, with: Entities::CommitNote else - not_found! + render_api_error!("Failed to save note #{note.errors.messages}", 400) end end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 42e4442365d..b23eff3661c 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -14,10 +14,14 @@ module API expose :bio, :skype, :linkedin, :twitter, :website_url end + class Identity < Grape::Entity + expose :provider, :extern_uid + end + class UserFull < User expose :email - expose :theme_id, :color_scheme_id, :extern_uid, :provider, \ - :projects_limit + expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at + expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project end @@ -42,7 +46,7 @@ module API end class Project < Grape::Entity - expose :id, :description, :default_branch + expose :id, :description, :default_branch, :tag_list expose :public?, as: :public expose :archived?, as: :archived expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url @@ -50,8 +54,10 @@ module API expose :name, :name_with_namespace expose :path, :path_with_namespace expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at + expose :creator_id expose :namespace - expose :forked_from_project, using: Entities::ForkedFromProject, :if => lambda{ | project, options | project.forked? } + expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } + expose :avatar_url end class ProjectMember < UserBasic @@ -61,7 +67,7 @@ module API end class Group < Grape::Entity - expose :id, :name, :path, :owner_id + expose :id, :name, :path, :description end class GroupDetail < Group @@ -138,11 +144,16 @@ module API class ProjectEntity < Grape::Entity expose :id, :iid - expose (:project_id) { |entity| entity.project.id } + expose(:project_id) { |entity| entity.project.id } expose :title, :description expose :state, :created_at, :updated_at end + class RepoDiff < Grape::Entity + expose :old_path, :new_path, :a_mode, :b_mode, :diff + expose :new_file, :renamed_file, :deleted_file + end + class Milestone < ProjectEntity expose :due_date end @@ -162,6 +173,12 @@ module API expose :milestone, using: Entities::Milestone end + class MergeRequestChanges < MergeRequest + expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _| + compare.diffs + end + end + class SSHKey < Grape::Entity expose :id, :title, :key, :created_at end @@ -232,18 +249,13 @@ module API expose :name, :color end - class RepoDiff < Grape::Entity - expose :old_path, :new_path, :a_mode, :b_mode, :diff - expose :new_file, :renamed_file, :deleted_file - end - class Compare < Grape::Entity expose :commit, using: Entities::RepoCommit do |compare, options| - Commit.decorate(compare.commits).last + Commit.decorate(compare.commits, nil).last end expose :commits, using: Entities::RepoCommit do |compare, options| - Commit.decorate(compare.commits) + Commit.decorate(compare.commits, nil) end expose :diffs, using: Entities::RepoDiff do |compare, options| @@ -260,5 +272,9 @@ module API class Contributor < Grape::Entity expose :name, :email, :commits, :additions, :deletions end + + class BroadcastMessage < Grape::Entity + expose :message, :starts_at, :ends_at, :color, :font + end end end diff --git a/lib/api/files.rb b/lib/api/files.rb index 84e1d311781..c7b30cf2f07 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -3,6 +3,26 @@ module API class Files < Grape::API before { authenticate! } + helpers do + def commit_params(attrs) + { + file_path: attrs[:file_path], + current_branch: attrs[:branch_name], + target_branch: attrs[:branch_name], + commit_message: attrs[:commit_message], + file_content: attrs[:content], + file_content_encoding: attrs[:encoding] + } + end + + def commit_response(attrs) + { + file_path: attrs[:file_path], + branch_name: attrs[:branch_name], + } + end + end + resource :projects do # Get file from repository # File content is Base64 encoded @@ -34,8 +54,8 @@ module API ref = attrs.delete(:ref) file_path = attrs.delete(:file_path) - commit = user_project.repository.commit(ref) - not_found! "Commit" unless commit + commit = user_project.commit(ref) + not_found! 'Commit' unless commit blob = user_project.repository.blob_at(commit.sha, file_path) @@ -53,7 +73,7 @@ module API commit_id: commit.id, } else - render_api_error!('File not found', 404) + not_found! 'File' end end @@ -73,17 +93,11 @@ module API required_attributes! [:file_path, :branch_name, :content, :commit_message] attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding] - branch_name = attrs.delete(:branch_name) - file_path = attrs.delete(:file_path) - result = ::Files::CreateService.new(user_project, current_user, attrs, branch_name, file_path).execute + result = ::Files::CreateService.new(user_project, current_user, commit_params(attrs)).execute if result[:status] == :success status(201) - - { - file_path: file_path, - branch_name: branch_name - } + commit_response(attrs) else render_api_error!(result[:message], 400) end @@ -105,19 +119,14 @@ module API required_attributes! [:file_path, :branch_name, :content, :commit_message] attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding] - branch_name = attrs.delete(:branch_name) - file_path = attrs.delete(:file_path) - result = ::Files::UpdateService.new(user_project, current_user, attrs, branch_name, file_path).execute + result = ::Files::UpdateService.new(user_project, current_user, commit_params(attrs)).execute if result[:status] == :success status(200) - - { - file_path: file_path, - branch_name: branch_name - } + commit_response(attrs) else - render_api_error!(result[:message], 400) + http_status = result[:http_status] || 400 + render_api_error!(result[:message], http_status) end end @@ -137,17 +146,11 @@ module API required_attributes! [:file_path, :branch_name, :commit_message] attrs = attributes_for_keys [:file_path, :branch_name, :commit_message] - branch_name = attrs.delete(:branch_name) - file_path = attrs.delete(:file_path) - result = ::Files::DeleteService.new(user_project, current_user, attrs, branch_name, file_path).execute + result = ::Files::DeleteService.new(user_project, current_user, commit_params(attrs)).execute if result[:status] == :success status(200) - - { - file_path: file_path, - branch_name: branch_name - } + commit_response(attrs) else render_api_error!(result[:message], 400) end diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb index d596517c816..ab9b7c602b5 100644 --- a/lib/api/group_members.rb +++ b/lib/api/group_members.rb @@ -3,30 +3,13 @@ module API before { authenticate! } resource :groups do - helpers do - def find_group(id) - group = Group.find(id) - - if can?(current_user, :read_group, group) - group - else - render_api_error!("403 Forbidden - #{current_user.username} lacks sufficient access to #{group.name}", 403) - end - end - - def validate_access_level?(level) - Gitlab::Access.options_with_owner.values.include? level.to_i - end - end - # Get a list of group members viewable by the authenticated user. # # Example Request: # GET /groups/:id/members get ":id/members" do group = find_group(params[:id]) - members = group.group_members - users = (paginate members).collect(&:user) + users = group.users present users, with: Entities::GroupMember, group: group end @@ -40,7 +23,7 @@ module API # POST /groups/:id/members post ":id/members" do group = find_group(params[:id]) - authorize! :manage_group, group + authorize! :admin_group, group required_attributes! [:user_id, :access_level] unless validate_access_level?(params[:access_level]) @@ -51,11 +34,35 @@ module API render_api_error!("Already exists", 409) end - group.add_users([params[:user_id]], params[:access_level]) + group.add_users([params[:user_id]], params[:access_level], current_user) member = group.group_members.find_by(user_id: params[:user_id]) present member.user, with: Entities::GroupMember, group: group end + # Update group member + # + # Parameters: + # id (required) - The ID of a group + # user_id (required) - The ID of a group member + # access_level (required) - Project access level + # Example Request: + # PUT /groups/:id/members/:user_id + put ':id/members/:user_id' do + group = find_group(params[:id]) + authorize! :admin_group, group + required_attributes! [:access_level] + + group_member = group.group_members.find_by(user_id: params[:user_id]) + not_found!('User can not be found') if group_member.nil? + + if group_member.update_attributes(access_level: params[:access_level]) + @member = group_member.user + present @member, with: Entities::GroupMember, group: group + else + handle_member_errors group_member.errors + end + end + # Remove member. # # Parameters: @@ -66,7 +73,7 @@ module API # DELETE /groups/:id/members/:user_id delete ":id/members/:user_id" do group = find_group(params[:id]) - authorize! :manage_group, group + authorize! :admin_group, group member = group.group_members.find_by(user_id: params[:user_id]) if member.nil? diff --git a/lib/api/groups.rb b/lib/api/groups.rb index f0ab6938b1c..e88b6e31775 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -4,36 +4,23 @@ module API before { authenticate! } resource :groups do - helpers do - def find_group(id) - group = Group.find(id) - - if can?(current_user, :read_group, group) - group - else - render_api_error!("403 Forbidden - #{current_user.username} lacks sufficient access to #{group.name}", 403) - end - end - - def validate_access_level?(level) - Gitlab::Access.options_with_owner.values.include? level.to_i - end - end - # Get a groups list # # Example Request: # GET /groups get do - if current_user.admin - @groups = paginate Group - else - @groups = paginate current_user.groups - end + @groups = if current_user.admin + Group.all + else + current_user.groups + end + + @groups = @groups.search(params[:search]) if params[:search].present? + @groups = paginate @groups present @groups, with: Entities::Group end - # Create group. Available only for admin + # Create group. Available only for users who can create groups. # # Parameters: # name (required) - The name of the group @@ -41,17 +28,17 @@ module API # Example Request: # POST /groups post do - authenticated_as_admin! + authorize! :create_group, current_user required_attributes! [:name, :path] - attrs = attributes_for_keys [:name, :path] + attrs = attributes_for_keys [:name, :path, :description] @group = Group.new(attrs) - @group.owner = current_user if @group.save + @group.add_owner(current_user) present @group, with: Entities::Group else - not_found! + render_api_error!("Failed to save group #{@group.errors.messages}", 400) end end @@ -74,8 +61,8 @@ module API # DELETE /groups/:id delete ":id" do group = find_group(params[:id]) - authorize! :manage_group, group - group.destroy + authorize! :admin_group, group + DestroyGroupService.new(group, current_user).execute end # Transfer a project to the Group namespace @@ -94,7 +81,7 @@ module API if result present group else - not_found! + render_api_error!("Failed to transfer project #{project.errors.messages}", 400) end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 027fb20ec46..1ebf9a1f022 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -11,7 +11,7 @@ module API def current_user private_token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s - @current_user ||= User.find_by(authentication_token: private_token) + @current_user ||= (User.find_by(authentication_token: private_token) || doorkeeper_guard) unless @current_user && Gitlab::UserAccess.allowed?(@current_user) return nil @@ -20,7 +20,7 @@ module API identifier = sudo_identifier() # If the sudo is the current user do nothing - if (identifier && !(@current_user.id == identifier || @current_user.username == identifier)) + if identifier && !(@current_user.id == identifier || @current_user.username == identifier) render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin? @current_user = User.by_username_or_id(identifier) not_found!("No user id or username for: #{identifier}") if @current_user.nil? @@ -33,7 +33,7 @@ module API identifier ||= params[SUDO_PARAM] ||= env[SUDO_HEADER] # Regex for integers - if (!!(identifier =~ /^[0-9]+$/)) + if !!(identifier =~ /^[0-9]+$/) identifier.to_i else identifier @@ -42,7 +42,7 @@ module API def user_project @project ||= find_project(params[:id]) - @project || not_found! + @project || not_found!("Project") end def find_project(id) @@ -55,6 +55,21 @@ module API end end + def find_group(id) + begin + group = Group.find(id) + rescue ActiveRecord::RecordNotFound + group = Group.find_by!(path: id) + end + + if can?(current_user, :read_group, group) + group + else + forbidden!("#{current_user.username} lacks sufficient "\ + "access to #{group.name}") + end + end + def paginate(relation) per_page = params[:per_page].to_i paginated = relation.page(params[:page]).per(per_page) @@ -68,7 +83,10 @@ module API end def authenticate_by_gitlab_shell_token! - unauthorized! unless secret_token == params['secret_token'] + input = params['secret_token'].try(:chomp) + unless Devise.secure_compare(secret_token, input) + unauthorized! + end end def authenticated_as_admin! @@ -135,10 +153,36 @@ module API errors end + def validate_access_level?(level) + Gitlab::Access.options_with_owner.values.include? level.to_i + end + + def issuable_order_by + if params["order_by"] == 'updated_at' + 'updated_at' + else + 'created_at' + end + end + + def issuable_sort + if params["sort"] == 'asc' + :asc + else + :desc + end + end + + def filter_by_iid(items, iid) + items.where(iid: iid) + end + # error helpers - def forbidden! - render_api_error!('403 Forbidden', 403) + def forbidden!(reason = nil) + message = ['403 Forbidden'] + message << " - #{reason}" if reason + render_api_error!(message.join(' '), 403) end def bad_request!(attribute) @@ -167,13 +211,13 @@ module API end def render_validation_error!(model) - unless model.valid? + if model.errors.any? render_api_error!(model.errors.messages || '400 Bad Request', 400) end end def render_api_error!(message, status) - error!({'message' => message}, status) + error!({ 'message' => message }, status) end private @@ -199,7 +243,12 @@ module API end def secret_token - File.read(Rails.root.join('.gitlab_shell_secret')) + File.read(Gitlab.config.gitlab_shell.secret_file).chomp + end + + def handle_member_errors(errors) + error!(errors[:access_level], 422) if errors[:access_level].any? + not_found!(errors) end end end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 180e50611cf..e38736fc28b 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -1,9 +1,7 @@ module API # Internal access API class Internal < Grape::API - before { - authenticate_by_gitlab_shell_token! - } + before { authenticate_by_gitlab_shell_token! } namespace 'internal' do # Check if git command is allowed to project @@ -18,42 +16,33 @@ module API # post "/allowed" do status 200 - project_path = params[:project] + actor = + if params[:key_id] + Key.find_by(id: params[:key_id]) + elsif params[:user_id] + User.find_by(id: params[:user_id]) + end + + project_path = params[:project] + # Check for *.wiki repositories. # Strip out the .wiki from the pathname before finding the # project. This applies the correct project permissions to # the wiki repository as well. - access = - if project_path =~ /\.wiki\Z/ - project_path.sub!(/\.wiki\Z/, '') - Gitlab::GitAccessWiki.new - else - Gitlab::GitAccess.new - end + wiki = project_path.end_with?('.wiki') + project_path.chomp!('.wiki') if wiki project = Project.find_with_namespace(project_path) - unless project - return Gitlab::GitAccessStatus.new(false, 'No such project') - end - - actor = if params[:key_id] - Key.find_by(id: params[:key_id]) - elsif params[:user_id] - User.find_by(id: params[:user_id]) - end - - unless actor - return Gitlab::GitAccessStatus.new(false, 'No such user or key') - end + access = + if wiki + Gitlab::GitAccessWiki.new(actor, project) + else + Gitlab::GitAccess.new(actor, project) + end - access.check( - actor, - params[:action], - project, - params[:changes] - ) + access.check(params[:action], params[:changes]) end # @@ -71,6 +60,14 @@ module API gitlab_rev: Gitlab::REVISION, } end + + get "/broadcast_message" do + if message = BroadcastMessage.current + present message, with: Entities::BroadcastMessage + else + {} + end + end end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index d2828b24c36..c8db93eb778 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -27,7 +27,9 @@ module API # Parameters: # state (optional) - Return "opened" or "closed" issues # labels (optional) - Comma-separated list of label names - + # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` + # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` + # # Example Requests: # GET /issues # GET /issues?state=opened @@ -39,8 +41,7 @@ module API issues = current_user.issues issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? - issues = issues.order('issues.id DESC') - + issues.reorder(issuable_order_by => issuable_sort) present paginate(issues), with: Entities::Issue end end @@ -50,9 +51,12 @@ module API # # Parameters: # id (required) - The ID of a project + # iid (optional) - Return the project issue having the given `iid` # state (optional) - Return "opened" or "closed" issues # labels (optional) - Comma-separated list of label names # milestone (optional) - Milestone title + # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` + # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` # # Example Requests: # GET /projects/:id/issues @@ -63,15 +67,18 @@ module API # GET /projects/:id/issues?labels=foo,bar&state=opened # GET /projects/:id/issues?milestone=1.0.0 # GET /projects/:id/issues?milestone=1.0.0&state=closed + # GET /issues?iid=42 get ":id/issues" do issues = user_project.issues 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? + unless params[:milestone].nil? issues = filter_issues_milestone(issues, params[:milestone]) end - issues = issues.order('issues.id DESC') + issues.reorder(issuable_order_by => issuable_sort) present paginate(issues), with: Entities::Issue end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index a365f1db00f..d835dce2ded 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -24,7 +24,10 @@ module API # # Parameters: # id (required) - The ID of a project + # iid (optional) - Return the project MR having the given `iid` # state (optional) - Return requests "merged", "opened" or "closed" + # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` + # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` # # Example: # GET /projects/:id/merge_requests @@ -34,28 +37,26 @@ module API # GET /projects/:id/merge_requests?order_by=updated_at # GET /projects/:id/merge_requests?sort=desc # GET /projects/:id/merge_requests?sort=asc + # GET /projects/:id/merge_requests?iid=42 # get ":id/merge_requests" do authorize! :read_merge_request, user_project + merge_requests = user_project.merge_requests - mrs = case params["state"] - when "opened" then user_project.merge_requests.opened - when "closed" then user_project.merge_requests.closed - when "merged" then user_project.merge_requests.merged - else user_project.merge_requests - end - - sort = case params["sort"] - when 'desc' then 'DESC' - else 'ASC' - end + unless params[:iid].nil? + merge_requests = filter_by_iid(merge_requests, params[:iid]) + end - mrs = case params["order_by"] - when 'updated_at' then mrs.order("updated_at #{sort}") - else mrs.order("created_at #{sort}") - end + merge_requests = + case params["state"] + when "opened" then merge_requests.opened + when "closed" then merge_requests.closed + when "merged" then merge_requests.merged + else merge_requests + end - present paginate(mrs), with: Entities::MergeRequest + merge_requests.reorder(issuable_order_by => issuable_sort) + present paginate(merge_requests), with: Entities::MergeRequest end # Show MR @@ -75,6 +76,22 @@ module API present merge_request, with: Entities::MergeRequest end + # Show MR changes + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of MR + # + # Example: + # GET /projects/:id/merge_request/:merge_request_id/changes + # + get ':id/merge_request/:merge_request_id/changes' do + merge_request = user_project.merge_requests. + find(params[:merge_request_id]) + authorize! :read_merge_request, merge_request + present merge_request, with: Entities::MergeRequestChanges + end + # Create MR # # Parameters: @@ -120,7 +137,6 @@ module API # Parameters: # id (required) - The ID of a project # merge_request_id (required) - ID of MR - # source_branch - The source branch # target_branch - The target branch # assignee_id - Assignee user ID # title - Title of MR @@ -131,10 +147,15 @@ module API # PUT /projects/:id/merge_request/:merge_request_id # put ":id/merge_request/:merge_request_id" do - attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :state_event, :description] + attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description] merge_request = user_project.merge_requests.find(params[:merge_request_id]) authorize! :modify_merge_request, merge_request + # Ensure source_branch is not specified + if params[:source_branch].present? + render_api_error!('Source branch cannot be changed', 400) + end + # Validate label names in advance if (errors = validate_label_params(params)).any? render_api_error!({ labels: errors }, 400) @@ -158,8 +179,8 @@ module API # Merge MR # # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR # merge_commit_message (optional) - Custom merge commit message # Example: # PUT /projects/:id/merge_request/:merge_request_id/merge @@ -167,18 +188,15 @@ module API put ":id/merge_request/:merge_request_id/merge" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) - action = if user_project.protected_branch?(merge_request.target_branch) - :push_code_to_protected_branches - else - :push_code - end + allowed = ::Gitlab::GitAccess.new(current_user, user_project). + can_push_to_branch?(merge_request.target_branch) - if can?(current_user, action, user_project) + if allowed if merge_request.unchecked? merge_request.check_if_can_be_merged end - if merge_request.open? + if merge_request.open? && !merge_request.work_in_progress? if merge_request.can_be_merged? merge_request.automerge!(current_user, params[:merge_commit_message] || merge_request.merge_commit_message) present merge_request, with: Entities::MergeRequest @@ -187,7 +205,7 @@ module API end else # Merge request can not be merged - # because it is already closed/merged + # because it is already closed/merged or marked as WIP not_allowed! end else @@ -201,7 +219,7 @@ module API # Get a merge request's comments # # Parameters: - # id (required) - The ID of a project + # id (required) - The ID of a project # merge_request_id (required) - ID of MR # Examples: # GET /projects/:id/merge_request/:merge_request_id/comments @@ -217,9 +235,9 @@ module API # Post comment to merge request # # Parameters: - # id (required) - The ID of a project + # id (required) - The ID of a project # merge_request_id (required) - ID of MR - # note (required) - Text of comment + # note (required) - Text of comment # Examples: # POST /projects/:id/merge_request/:merge_request_id/comments # @@ -233,7 +251,7 @@ module API if note.save present note, with: Entities::MRNote else - render_validation_error!(note) + render_api_error!("Failed to save note #{note.errors.messages}", 400) end end end diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index a4fdb752d69..c5cd73943fb 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -48,7 +48,7 @@ module API if milestone.valid? present milestone, with: Entities::Milestone else - not_found! + render_api_error!("Failed to create milestone #{milestone.errors.messages}", 400) end end @@ -72,9 +72,24 @@ module API if milestone.valid? present milestone, with: Entities::Milestone else - not_found! + render_api_error!("Failed to update milestone #{milestone.errors.messages}", 400) end end + + # Get all issues for a single project milestone + # + # Parameters: + # id (required) - The ID of a project + # milestone_id (required) - The ID of a project milestone + # Example Request: + # GET /projects/:id/milestones/:milestone_id/issues + get ":id/milestones/:milestone_id/issues" do + authorize! :read_milestone, user_project + + @milestone = user_project.milestones.find(params[:milestone_id]) + present paginate(@milestone.issues), with: Entities::Issue + end + end end end diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index f9f2ed90ccc..50d3729449e 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -1,10 +1,7 @@ module API # namespaces API class Namespaces < Grape::API - before { - authenticate! - authenticated_as_admin! - } + before { authenticate! } resource :namespaces do # Get a namespaces list @@ -12,7 +9,11 @@ module API # Example Request: # GET /namespaces get do - @namespaces = Namespace.all + @namespaces = if current_user.admin + Namespace.all + else + current_user.namespaces + end @namespaces = @namespaces.search(params[:search]) if params[:search].present? @namespaces = paginate @namespaces diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 0ef9a3c4beb..3726be7c537 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -61,9 +61,42 @@ module API if @note.valid? present @note, with: Entities::Note else - not_found! + not_found!("Note #{@note.errors.messages}") end end + + # Modify existing +noteable+ note + # + # Parameters: + # id (required) - The ID of a project + # noteable_id (required) - The ID of an issue or snippet + # node_id (required) - The ID of a note + # body (required) - New content of a note + # Example Request: + # PUT /projects/:id/issues/:noteable_id/notes/:note_id + # PUT /projects/:id/snippets/:noteable_id/notes/:node_id + put ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do + required_attributes! [:body] + + authorize! :admin_note, user_project.notes.find(params[:note_id]) + + opts = { + note: params[:body], + note_id: params[:note_id], + noteable_type: noteables_str.classify, + noteable_id: params[noteable_id_str] + } + + @note = ::Notes::UpdateService.new(user_project, current_user, + opts).execute + + if @note.valid? + present @note, with: Entities::Note + else + render_api_error!("Failed to save note #{note.errors.messages}", 400) + end + end + end end end diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 7d056b9bf58..ad4d2e65dfd 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -43,7 +43,8 @@ module API :push_events, :issues_events, :merge_requests_events, - :tag_push_events + :tag_push_events, + :note_events ] @hook = user_project.hooks.new(attrs) @@ -53,7 +54,7 @@ module API if @hook.errors[:url].present? error!("Invalid url given", 422) end - not_found! + not_found!("Project hook #{@hook.errors.messages}") end end @@ -73,7 +74,8 @@ module API :push_events, :issues_events, :merge_requests_events, - :tag_push_events + :tag_push_events, + :note_events ] if @hook.update_attributes attrs @@ -82,7 +84,7 @@ module API if @hook.errors[:url].present? error!("Invalid url given", 422) end - not_found! + not_found!("Project hook #{@hook.errors.messages}") end end diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb index 1595ed0bc36..c756bb479fc 100644 --- a/lib/api/project_members.rb +++ b/lib/api/project_members.rb @@ -4,14 +4,6 @@ module API before { authenticate! } resource :projects do - helpers do - def handle_project_member_errors(errors) - if errors[:access_level].any? - error!(errors[:access_level], 422) - end - not_found! - end - end # Get a project team members # @@ -54,19 +46,19 @@ module API required_attributes! [:user_id, :access_level] # either the user is already a team member or a new one - team_member = user_project.team_member_by_id(params[:user_id]) - if team_member.nil? - team_member = user_project.project_members.new( + project_member = user_project.project_member_by_id(params[:user_id]) + if project_member.nil? + project_member = user_project.project_members.new( user_id: params[:user_id], access_level: params[:access_level] ) end - if team_member.save - @member = team_member.user + if project_member.save + @member = project_member.user present @member, with: Entities::ProjectMember, project: user_project else - handle_project_member_errors team_member.errors + handle_member_errors project_member.errors end end @@ -82,14 +74,14 @@ module API authorize! :admin_project, user_project required_attributes! [:access_level] - team_member = user_project.project_members.find_by(user_id: params[:user_id]) - not_found!("User can not be found") if team_member.nil? + project_member = user_project.project_members.find_by(user_id: params[:user_id]) + not_found!("User can not be found") if project_member.nil? - if team_member.update_attributes(access_level: params[:access_level]) - @member = team_member.user + if project_member.update_attributes(access_level: params[:access_level]) + @member = project_member.user present @member, with: Entities::ProjectMember, project: user_project else - handle_project_member_errors team_member.errors + handle_member_errors project_member.errors end end @@ -102,11 +94,11 @@ module API # DELETE /projects/:id/members/:user_id delete ":id/members/:user_id" do authorize! :admin_project, user_project - team_member = user_project.project_members.find_by(user_id: params[:user_id]) - unless team_member.nil? - team_member.destroy + project_member = user_project.project_members.find_by(user_id: params[:user_id]) + unless project_member.nil? + project_member.destroy else - {message: "Access revoked", id: params[:user_id].to_i} + { message: "Access revoked", id: params[:user_id].to_i } end end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 0c2d282f785..54f2555903f 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -42,21 +42,22 @@ module API # title (required) - The title of a snippet # file_name (required) - The name of a snippet file # code (required) - The content of a snippet + # visibility_level (required) - The snippet's visibility # Example Request: # POST /projects/:id/snippets post ":id/snippets" do authorize! :write_project_snippet, user_project - required_attributes! [:title, :file_name, :code] + required_attributes! [:title, :file_name, :code, :visibility_level] - attrs = attributes_for_keys [:title, :file_name] + attrs = attributes_for_keys [:title, :file_name, :visibility_level] attrs[:content] = params[:code] if params[:code].present? - @snippet = user_project.snippets.new attrs - @snippet.author = current_user + @snippet = CreateSnippetService.new(user_project, current_user, + attrs).execute - if @snippet.save - present @snippet, with: Entities::ProjectSnippet - else + if @snippet.errors.any? render_validation_error!(@snippet) + else + present @snippet, with: Entities::ProjectSnippet end end @@ -68,19 +69,22 @@ module API # title (optional) - The title of a snippet # file_name (optional) - The name of a snippet file # code (optional) - The content of a snippet + # visibility_level (optional) - The snippet's visibility # Example Request: # PUT /projects/:id/snippets/:snippet_id put ":id/snippets/:snippet_id" do @snippet = user_project.snippets.find(params[:snippet_id]) authorize! :modify_project_snippet, @snippet - attrs = attributes_for_keys [:title, :file_name] + attrs = attributes_for_keys [:title, :file_name, :visibility_level] attrs[:content] = params[:code] if params[:code].present? - if @snippet.update_attributes attrs - present @snippet, with: Entities::ProjectSnippet - else + UpdateSnippetService.new(user_project, current_user, @snippet, + attrs).execute + if @snippet.errors.any? render_validation_error!(@snippet) + else + present @snippet, with: Entities::ProjectSnippet end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 7fcf97d1ad6..1f2251c9b9c 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -11,23 +11,51 @@ module API attrs[:visibility_level] = Gitlab::VisibilityLevel::PUBLIC if !attrs[:visibility_level].present? && publik == true attrs end + + def filter_projects(projects) + # If the archived parameter is passed, limit results accordingly + if params[:archived].present? + projects = projects.where(archived: parse_boolean(params[:archived])) + end + + if params[:search].present? + projects = projects.search(params[:search]) + end + + if params[:ci_enabled_first].present? + projects.includes(:gitlab_ci_service). + reorder("services.active DESC, projects.#{project_order_by} #{project_sort}") + else + projects.reorder(project_order_by => project_sort) + end + end + + def project_order_by + order_fields = %w(id name path created_at updated_at last_activity_at) + + if order_fields.include?(params['order_by']) + params['order_by'] + else + 'created_at' + end + end + + def project_sort + if params["sort"] == 'asc' + :asc + else + :desc + end + end end # Get a projects list for authenticated user # - # Parameters: - # archived (optional) - if passed, limit by archived status - # # Example Request: # GET /projects get do @projects = current_user.authorized_projects - - # If the archived parameter is passed, limit results accordingly - if params[:archived].present? - @projects = @projects.where(archived: parse_boolean(params[:archived])) - end - + @projects = filter_projects(@projects) @projects = paginate @projects present @projects, with: Entities::Project end @@ -37,7 +65,9 @@ module API # Example Request: # GET /projects/owned get '/owned' do - @projects = paginate current_user.owned_projects + @projects = current_user.owned_projects + @projects = filter_projects(@projects) + @projects = paginate @projects present @projects, with: Entities::Project end @@ -47,7 +77,9 @@ module API # GET /projects/all get '/all' do authenticated_as_admin! - @projects = paginate Project + @projects = Project.all + @projects = filter_projects(@projects) + @projects = paginate @projects present @projects, with: Entities::Project end @@ -61,17 +93,14 @@ module API present user_project, with: Entities::ProjectWithAccess, user: current_user end - # Get a single project events + # Get events for a single project # # Parameters: # id (required) - The ID of a project # Example Request: - # GET /projects/:id + # GET /projects/:id/events get ":id/events" do - limit = (params[:per_page] || 20).to_i - offset = (params[:page] || 0).to_i * limit - events = user_project.events.recent.limit(limit).offset(offset) - + events = paginate user_project.events.recent present events, with: Entities::Event end @@ -170,6 +199,49 @@ module API end end + # Update an existing project + # + # Parameters: + # id (required) - the id of a project + # name (optional) - name of a project + # path (optional) - path of a project + # description (optional) - short project description + # issues_enabled (optional) + # merge_requests_enabled (optional) + # wiki_enabled (optional) + # snippets_enabled (optional) + # public (optional) - if true same as setting visibility_level = 20 + # visibility_level (optional) - visibility level of a project + # Example Request + # PUT /projects/:id + put ':id' do + attrs = attributes_for_keys [:name, + :path, + :description, + :default_branch, + :issues_enabled, + :merge_requests_enabled, + :wiki_enabled, + :snippets_enabled, + :public, + :visibility_level] + attrs = map_public_to_visibility_level(attrs) + authorize_admin_project + authorize! :rename_project, user_project if attrs[:name].present? + if attrs[:visibility_level].present? + authorize! :change_visibility_level, user_project + end + + ::Projects::UpdateService.new(user_project, + current_user, attrs).execute + + if user_project.errors.any? + render_validation_error!(user_project) + else + present user_project, with: Entities::Project + end + end + # Remove project # # Parameters: @@ -198,7 +270,7 @@ module API render_api_error!("Project already forked", 409) end else - not_found! + not_found!("Source Project") end end @@ -227,6 +299,16 @@ module API ids = current_user.authorized_projects.map(&:id) visibility_levels = [ Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC ] projects = Project.where("(id in (?) OR visibility_level in (?)) AND (name LIKE (?))", ids, visibility_levels, "%#{params[:query]}%") + sort = params[:sort] == 'desc' ? 'desc' : 'asc' + + projects = case params["order_by"] + when 'id' then projects.order("id #{sort}") + when 'name' then projects.order("name #{sort}") + when 'created_at' then projects.order("created_at #{sort}") + when 'last_activity_at' then projects.order("last_activity_at #{sort}") + else projects + end + present paginate(projects), with: Entities::Project end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index a1a7721b288..2d96c9666d2 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -58,11 +58,13 @@ module API # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used # Example Request: # GET /projects/:id/repository/tree - get ":id/repository/tree" do + get ':id/repository/tree' do ref = params[:ref_name] || user_project.try(:default_branch) || 'master' path = params[:path] || nil - commit = user_project.repository.commit(ref) + commit = user_project.commit(ref) + not_found!('Tree') unless commit + tree = user_project.repository.tree(commit.id, path) present tree.sorted_entries, with: Entities::RepoTreeObject @@ -100,14 +102,18 @@ module API # sha (required) - The blob's sha # Example Request: # GET /projects/:id/repository/raw_blobs/:sha - get ":id/repository/raw_blobs/:sha" do + get ':id/repository/raw_blobs/:sha' do ref = params[:sha] repo = user_project.repository - blob = Gitlab::Git::Blob.raw(repo, ref) + begin + blob = Gitlab::Git::Blob.raw(repo, ref) + rescue + not_found! 'Blob' + end - not_found! "Blob" unless blob + not_found! 'Blob' unless blob env['api.format'] = :txt @@ -122,18 +128,29 @@ module API # sha (optional) - the commit sha to download defaults to the tip of the default branch # Example Request: # GET /projects/:id/repository/archive - get ":id/repository/archive", requirements: { format: Gitlab::Regex.archive_formats_regex } do + get ':id/repository/archive', + requirements: { format: Gitlab::Regex.archive_formats_regex } do authorize! :download_code, user_project - file_path = ArchiveRepositoryService.new.execute(user_project, params[:sha], params[:format]) + + begin + file_path = ArchiveRepositoryService.new( + user_project, + params[:sha], + params[:format] + ).execute + rescue + not_found!('File') + end if file_path && File.exists?(file_path) data = File.open(file_path, 'rb').read - header["Content-Disposition"] = "attachment; filename=\"#{File.basename(file_path)}\"" + basename = File.basename(file_path) + header['Content-Disposition'] = "attachment; filename=\"#{basename}\"" content_type MIME::Types.type_for(file_path).first.content_type env['api.format'] = :binary present data else - not_found! + redirect request.fullpath end end @@ -161,7 +178,12 @@ module API get ':id/repository/contributors' do authorize! :download_code, user_project - present user_project.repository.contributors, with: Entities::Contributor + begin + present user_project.repository.contributors, + with: Entities::Contributor + rescue + not_found! + end end end end diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 3e239c5afe7..22b8f90dc5c 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -1,10 +1,10 @@ module API # Hooks API class SystemHooks < Grape::API - before { + before do authenticate! authenticated_as_admin! - } + end resource :hooks do # Get the list of system hooks @@ -47,7 +47,7 @@ module API owner_name: "Someone", owner_email: "example@gitlabhq.com" } - @hook.execute(data) + @hook.execute(data, 'system_hooks') data end diff --git a/lib/api/users.rb b/lib/api/users.rb index d07815a8a97..7d4c68c7412 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -54,15 +54,24 @@ module API # bio - Bio # admin - User is admin - true or false (default) # can_create_group - User can create groups - true or false + # confirm - Require user confirmation - true (default) or false # Example Request: # POST /users post do authenticated_as_admin! required_attributes! [:email, :password, :name, :username] - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio, :can_create_group, :admin] - user = User.build_user(attrs) + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm] admin = attrs.delete(:admin) + 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 + + identity_attrs = attributes_for_keys [:provider, :extern_uid] + if identity_attrs.any? + user.identities.build(identity_attrs) + end + if user.save present user, with: Entities::UserFull else @@ -89,8 +98,6 @@ module API # twitter - Twitter account # website_url - Website url # projects_limit - Limit projects each user can create - # extern_uid - External authentication provider UID - # provider - External provider # bio - Bio # admin - User is admin - true or false (default) # can_create_group - User can create groups - true or false @@ -99,7 +106,7 @@ module API put ":id" do authenticated_as_admin! - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :extern_uid, :provider, :bio, :can_create_group, :admin] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin] user = User.find(params[:id]) not_found!('User') unless user @@ -187,7 +194,7 @@ module API user = User.find_by(id: params[:id]) if user - user.destroy + DeleteUserService.new.execute(user) else not_found!('User') end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index ab8db4e9837..6fa2079d1a8 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -1,7 +1,5 @@ module Backup class Manager - BACKUP_CONTENTS = %w{repositories/ db/ uploads/ backup_information.yml} - def pack # saving additional informations s = {} @@ -9,24 +7,30 @@ module Backup s[:backup_created_at] = Time.now s[:gitlab_version] = Gitlab::VERSION s[:tar_version] = tar_version + s[:skipped] = ENV["SKIP"] tar_file = "#{s[:backup_created_at].to_i}_gitlab_backup.tar" - Dir.chdir(Gitlab.config.backup.path) + Dir.chdir(Gitlab.config.backup.path) do + File.open("#{Gitlab.config.backup.path}/backup_information.yml", + "w+") do |file| + file << s.to_yaml.gsub(/^---\n/,'') + end - File.open("#{Gitlab.config.backup.path}/backup_information.yml", "w+") do |file| - file << s.to_yaml.gsub(/^---\n/,'') - end + FileUtils.chmod(0700, folders_to_backup) - # create archive - $progress.print "Creating backup archive: #{tar_file} ... " - if Kernel.system('tar', '-cf', tar_file, *BACKUP_CONTENTS) - $progress.puts "done".green - else - puts "creating archive #{tar_file} failed".red - abort 'Backup failed' - end + # create archive + $progress.print "Creating backup archive: #{tar_file} ... " + orig_umask = File.umask(0077) + if Kernel.system('tar', '-cf', tar_file, *backup_contents) + $progress.puts "done".green + else + puts "creating archive #{tar_file} failed".red + abort 'Backup failed' + end + File.umask(orig_umask) - upload(tar_file) + upload(tar_file) + end end def upload(tar_file) @@ -41,7 +45,9 @@ module Backup connection = ::Fog::Storage.new(connection_settings) directory = connection.directories.get(remote_directory) - if directory.files.create(key: tar_file, body: File.open(tar_file), public: false) + + if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, + multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size) $progress.puts "done".green else puts "uploading backup to #{remote_directory} failed".red @@ -51,11 +57,16 @@ module Backup def cleanup $progress.print "Deleting tmp directories ... " - if Kernel.system('rm', '-rf', *BACKUP_CONTENTS) - $progress.puts "done".green - else - puts "deleting tmp directory failed".red - abort 'Backup failed' + + backup_contents.each do |dir| + 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 + else + puts "deleting tmp directory '#{dir}' failed".red + abort 'Backup failed' + end end end @@ -63,19 +74,22 @@ module Backup # delete backups $progress.print "Deleting old backups ... " keep_time = Gitlab.config.backup.keep_time.to_i - path = Gitlab.config.backup.path if keep_time > 0 removed = 0 - file_list = Dir.glob(Rails.root.join(path, "*_gitlab_backup.tar")) - file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ } - file_list.sort.each do |timestamp| - if Time.at(timestamp) < (Time.now - keep_time) - if Kernel.system(*%W(rm #{timestamp}_gitlab_backup.tar)) - removed += 1 + + Dir.chdir(Gitlab.config.backup.path) do + file_list = Dir.glob('*_gitlab_backup.tar') + file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ } + file_list.sort.each do |timestamp| + if Time.at(timestamp) < (Time.now - keep_time) + if Kernel.system(*%W(rm #{timestamp}_gitlab_backup.tar)) + removed += 1 + end end end end + $progress.puts "done. (#{removed} removed)".green else $progress.puts "skipping".yellow @@ -88,6 +102,7 @@ module Backup # check for existing backups in the backup dir file_list = Dir.glob("*_gitlab_backup.tar").each.map { |f| f.split(/_/).first.to_i } puts "no backups found" if file_list.count == 0 + if file_list.count > 1 && ENV["BACKUP"].nil? puts "Found more than one backup, please specify which one you want to restore:" puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup" @@ -102,6 +117,7 @@ module Backup end $progress.print "Unpacking backup ... " + unless Kernel.system(*%W(tar -xf #{tar_file})) puts "unpacking backup failed".red exit 1 @@ -109,7 +125,6 @@ module Backup $progress.puts "done".green end - settings = YAML.load_file("backup_information.yml") ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 # restoring mismatching backups can lead to unexpected problems @@ -128,5 +143,29 @@ module Backup tar_version, _ = Gitlab::Popen.popen(%W(tar --version)) tar_version.force_encoding('locale').split("\n").first end + + def skipped?(item) + settings[:skipped] && settings[:skipped].include?(item) + end + + private + + def backup_contents + folders_to_backup + ["backup_information.yml"] + end + + def folders_to_backup + folders = %w{repositories db uploads} + + if ENV["SKIP"] + return folders.reject{ |folder| ENV["SKIP"].include?(folder) } + end + + folders + end + + def settings + @settings ||= YAML.load_file("backup_information.yml") + end end end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index e18bc804437..dfb2da9f84e 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -16,7 +16,7 @@ module Backup if project.empty_repo? $progress.puts "[SKIPPED]".cyan else - cmd = %W(git --git-dir=#{path_to_repo(project)} bundle create #{path_to_bundle(project)} --all) + 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 @@ -64,7 +64,8 @@ module Backup project.namespace.ensure_dir_exist if project.namespace if File.exists?(path_to_bundle(project)) - cmd = %W(git clone --bare #{path_to_bundle(project)} #{path_to_repo(project)}) + FileUtils.mkdir_p(path_to_repo(project)) + cmd = %W(tar -xf #{path_to_bundle(project)} -C #{path_to_repo(project)}) else cmd = %W(git init --bare #{path_to_repo(project)}) end diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index e50e1ff4f13..bf43610acf6 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -23,7 +23,7 @@ module Backup def backup_existing_uploads_dir timestamped_uploads_path = File.join(app_uploads_dir, '..', "uploads.#{Time.now.to_i}") if File.exists?(app_uploads_dir) - FileUtils.mv(app_uploads_dir, timestamped_uploads_path) + FileUtils.mv(app_uploads_dir, File.expand_path(timestamped_uploads_path)) end end end diff --git a/lib/email_validator.rb b/lib/email_validator.rb index 0a67ebcd795..f509f0a5843 100644 --- a/lib/email_validator.rb +++ b/lib/email_validator.rb @@ -1,5 +1,5 @@ # Based on https://github.com/balexand/email_validator -# +# # Extended to use only strict mode with following allowed characters: # ' - apostrophe # diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index e51cb30bdd9..6e4ed01e079 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -1,17 +1,9 @@ # Module providing methods for dealing with separating a tree-ish string and a # file path string when combined in a request parameter module ExtractsPath - extend ActiveSupport::Concern - # Raised when given an invalid file path class InvalidPathError < StandardError; end - included do - if respond_to?(:before_filter) - before_filter :assign_ref_vars - end - end - # Given a string containing both a Git tree-ish, such as a branch or tag, and # a filesystem path joined by forward slashes, attempts to separate the two. # @@ -110,7 +102,8 @@ module ExtractsPath raise InvalidPathError unless @commit @hex_path = Digest::SHA1.hexdigest(@path) - @logs_path = logs_file_project_ref_path(@project, @ref, @path) + @logs_path = logs_file_namespace_project_ref_path(@project.namespace, + @project, @ref, @path) rescue RuntimeError, NoMethodError, InvalidPathError not_found! diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb index 42970c1be59..2eae55e534b 100644 --- a/lib/file_size_validator.rb +++ b/lib/file_size_validator.rb @@ -25,8 +25,8 @@ class FileSizeValidator < ActiveModel::EachValidator keys.each do |key| value = options[key] - unless value.is_a?(Integer) && value >= 0 - raise ArgumentError, ":#{key} must be a nonnegative Integer" + unless (value.is_a?(Integer) && value >= 0) || value.is_a?(Symbol) + raise ArgumentError, ":#{key} must be a nonnegative Integer or symbol" end end end @@ -39,6 +39,14 @@ class FileSizeValidator < ActiveModel::EachValidator CHECKS.each do |key, validity_check| next unless check_value = options[key] + check_value = + case check_value + when Integer + check_value + when Symbol + record.send(check_value) + end + value ||= [] if key == :maximum value_size = value.size diff --git a/lib/gitlab.rb b/lib/gitlab.rb new file mode 100644 index 00000000000..5fc1862c3e9 --- /dev/null +++ b/lib/gitlab.rb @@ -0,0 +1,5 @@ +require 'gitlab/git' + +module Gitlab + autoload :Satellite, 'gitlab/satellite/satellite' +end diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index 411b2b9a3cc..6d0e30e916f 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -11,6 +11,11 @@ module Gitlab MASTER = 40 OWNER = 50 + # Branch protection settings + PROTECTION_NONE = 0 + PROTECTION_DEV_CAN_PUSH = 1 + PROTECTION_FULL = 2 + class << self def values options.values @@ -43,6 +48,18 @@ module Gitlab master: MASTER, } end + + def protection_options + { + "Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE, + "Partially protected: Developers can push new commits, but cannot force push or delete the branch. Masters can do all of those." => PROTECTION_DEV_CAN_PUSH, + "Fully protected: Developers cannot push new commits, force push, or delete the branch. Only masters can do any of those." => PROTECTION_FULL, + } + end + + def protection_values + protection_options.values + end end def human_access diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb new file mode 100644 index 00000000000..bf33e5b1b1e --- /dev/null +++ b/lib/gitlab/asciidoc.rb @@ -0,0 +1,60 @@ +require 'asciidoctor' +require 'html/pipeline' + +module Gitlab + # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters + # the resulting HTML through HTML pipeline filters. + module Asciidoc + + # Provide autoload paths for filters to prevent a circular dependency error + autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' + + DEFAULT_ADOC_ATTRS = [ + 'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab', + 'env-gitlab', 'source-highlighter=html-pipeline' + ].freeze + + # Public: Converts the provided Asciidoc markup into HTML. + # + # input - the source text in Asciidoc format + # context - a Hash with the template context: + # :commit + # :project + # :project_wiki + # :requested_path + # :ref + # asciidoc_opts - a Hash of options to pass to the Asciidoctor converter + # html_opts - a Hash of options for HTML output: + # :xhtml - output XHTML instead of HTML + # + def self.render(input, context, asciidoc_opts = {}, html_opts = {}) + asciidoc_opts = asciidoc_opts.reverse_merge( + safe: :secure, + backend: html_opts[:xhtml] ? :xhtml5 : :html5, + attributes: [] + ) + asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS) + + html = ::Asciidoctor.convert(input, asciidoc_opts) + + if context[:project] + result = HTML::Pipeline.new(filters).call(html, context) + + save_opts = html_opts[:xhtml] ? + Nokogiri::XML::Node::SaveOptions::AS_XHTML : 0 + + html = result[:output].to_html(save_with: save_opts) + end + + html.html_safe + end + + private + + def self.filters + [ + Gitlab::Markdown::RelativeLinkFilter + ] + end + end +end diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 762639414e0..03cef30c97d 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -10,8 +10,9 @@ module Grack @request = Rack::Request.new(env) @auth = Request.new(env) - # Need this patch due to the rails mount + @gitlab_ci = false + # Need this patch due to the rails mount # Need this if under RELATIVE_URL_ROOT unless Gitlab.config.gitlab.relative_url_root.empty? # If website is mounted using relative_url_root need to remove it first @@ -22,8 +23,12 @@ module Grack @env['SCRIPT_NAME'] = "" - if project - auth! + auth! + + if project && authorized_request? + @app.call(env) + elsif @user.nil? && !@gitlab_ci + unauthorized else render_not_found end @@ -32,35 +37,30 @@ module Grack private def auth! - if @auth.provided? - return bad_request unless @auth.basic? - - # Authentication with username and password - login, password = @auth.credentials + return unless @auth.provided? - # Allow authentication for GitLab CI service - # if valid token passed - if gitlab_ci_request?(login, password) - return @app.call(env) - end + return bad_request unless @auth.basic? - @user = authenticate_user(login, password) + # Authentication with username and password + login, password = @auth.credentials - if @user - Gitlab::ShellEnv.set_env(@user) - @env['REMOTE_USER'] = @auth.username - end + # Allow authentication for GitLab CI service + # if valid token passed + if gitlab_ci_request?(login, password) + @gitlab_ci = true + return end - if authorized_request? - @app.call(env) - else - unauthorized + @user = authenticate_user(login, password) + + if @user + Gitlab::ShellEnv.set_env(@user) + @env['REMOTE_USER'] = @auth.username end end def gitlab_ci_request?(login, password) - if login == "gitlab-ci-token" && project.gitlab_ci? + if login == "gitlab-ci-token" && project && project.gitlab_ci? token = project.gitlab_ci_service.token if token.present? && token == password && git_cmd == 'git-upload-pack' @@ -71,16 +71,64 @@ module Grack false end + def oauth_access_token_check(login, password) + if login == "oauth2" && git_cmd == 'git-upload-pack' && password.present? + token = Doorkeeper::AccessToken.by_token(password) + token && token.accessible? && User.find_by(id: token.resource_owner_id) + end + end + def authenticate_user(login, password) - auth = Gitlab::Auth.new - auth.find(login, password) + user = Gitlab::Auth.new.find(login, password) + + unless user + user = oauth_access_token_check(login, password) + end + + # If the user authenticated successfully, we reset the auth failure count + # from Rack::Attack for that IP. A client may attempt to authenticate + # with a username and blank password first, and only after it receives + # a 401 error does it present a password. Resetting the count prevents + # false positives from occurring. + # + # Otherwise, we let Rack::Attack know there was a failed authentication + # attempt from this IP. This information is stored in the Rails cache + # (Redis) and will be used by the Rack::Attack middleware to decide + # whether to block requests from this IP. + config = Gitlab.config.rack_attack.git_basic_auth + + if config.enabled + if user + # A successful login will reset the auth failure count from this IP + Rack::Attack::Allow2Ban.reset(@request.ip, config) + else + banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do + # Unless the IP is whitelisted, return true so that Allow2Ban + # increments the counter (stored in Rails.cache) for the IP + if config.ip_whitelist.include?(@request.ip) + false + else + true + end + end + + if banned + Rails.logger.info "IP #{@request.ip} failed to login " \ + "as #{login} but has been temporarily banned from Git auth" + end + end + end + + user end def authorized_request? + return true if @gitlab_ci + case git_cmd when *Gitlab::GitAccess::DOWNLOAD_COMMANDS if user - Gitlab::GitAccess.new.download_access_check(user, project).allowed? + Gitlab::GitAccess.new(user, project).download_access_check.allowed? elsif project.public? # Allow clone/fetch for public projects true @@ -111,7 +159,9 @@ module Grack end def project - @project ||= project_by_path(@request.path_info) + return @project if defined?(@project) + + @project = project_by_path(@request.path_info) end def project_by_path(path) @@ -119,12 +169,13 @@ module Grack path_with_namespace = m.last path_with_namespace.gsub!(/\.wiki$/, '') + path_with_namespace[0] = '' if path_with_namespace.start_with?('/') Project.find_with_namespace(path_with_namespace) end end def render_not_found - [404, {"Content-Type" => "text/plain"}, ["Not Found"]] + [404, { "Content-Type" => "text/plain" }, ["Not Found"]] end end end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index aabc7f1e69a..172d4902add 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -240,10 +240,20 @@ module Gitlab gitlab_shell_version_file = "#{gitlab_shell_path}/VERSION" if File.readable?(gitlab_shell_version_file) - File.read(gitlab_shell_version_file) + File.read(gitlab_shell_version_file).chomp end end + # Check if such directory exists in repositories. + # + # Usage: + # exists?('gitlab') + # exists?('gitlab/cookies.git') + # + def exists?(dir_name) + File.exists?(full_path(dir_name)) + end + protected def gitlab_shell_path @@ -264,10 +274,6 @@ module Gitlab File.join(repos_path, dir_name) end - def exists?(dir_name) - File.exists?(full_path(dir_name)) - end - def gitlab_shell_projects_path File.join(gitlab_shell_path, 'bin', 'gitlab-projects') end diff --git a/lib/gitlab/backend/shell_adapter.rb b/lib/gitlab/backend/shell_adapter.rb index f247f4593d7..fbe2a7a0d72 100644 --- a/lib/gitlab/backend/shell_adapter.rb +++ b/lib/gitlab/backend/shell_adapter.rb @@ -9,4 +9,3 @@ module Gitlab end end end - diff --git a/lib/gitlab/bitbucket_import.rb b/lib/gitlab/bitbucket_import.rb new file mode 100644 index 00000000000..7298152e7e9 --- /dev/null +++ b/lib/gitlab/bitbucket_import.rb @@ -0,0 +1,6 @@ +module Gitlab + module BitbucketImport + mattr_accessor :public_key + @public_key = nil + end +end diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb new file mode 100644 index 00000000000..5b1952b9675 --- /dev/null +++ b/lib/gitlab/bitbucket_import/client.rb @@ -0,0 +1,99 @@ +module Gitlab + module BitbucketImport + class Client + attr_reader :consumer, :api + + def initialize(access_token = nil, access_token_secret = nil) + @consumer = ::OAuth::Consumer.new( + config.app_id, + config.app_secret, + bitbucket_options + ) + + if access_token && access_token_secret + @api = ::OAuth::AccessToken.new(@consumer, access_token, access_token_secret) + end + end + + def request_token(redirect_uri) + request_token = consumer.get_request_token(oauth_callback: redirect_uri) + + { + oauth_token: request_token.token, + oauth_token_secret: request_token.secret, + oauth_callback_confirmed: request_token.callback_confirmed?.to_s + } + end + + def authorize_url(request_token, redirect_uri) + request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash) + + if request_token.callback_confirmed? + request_token.authorize_url + else + request_token.authorize_url(oauth_callback: redirect_uri) + end + end + + def get_token(request_token, oauth_verifier, redirect_uri) + request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash) + + if request_token.callback_confirmed? + request_token.get_access_token(oauth_verifier: oauth_verifier) + else + request_token.get_access_token(oauth_callback: redirect_uri) + end + end + + def user + JSON.parse(api.get("/api/1.0/user").body) + end + + def issues(project_identifier) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues").body) + end + + def issue_comments(project_identifier, issue_id) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body) + end + + def project(project_identifier) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}").body) + end + + def find_deploy_key(project_identifier, key) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| + deploy_key["key"].chomp == key.chomp + end + end + + def add_deploy_key(project_identifier, key) + deploy_key = find_deploy_key(project_identifier, key) + return if deploy_key + + JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body) + end + + def delete_deploy_key(project_identifier, key) + deploy_key = find_deploy_key(project_identifier, key) + return unless deploy_key + + api.delete("/api/1.0/repositories/#{project_identifier}/deploy-keys/#{deploy_key["pk"]}").code == "204" + end + + def projects + JSON.parse(api.get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" } + end + + private + + def config + Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket"} + end + + def bitbucket_options + OmniAuth::Strategies::Bitbucket.default_options[:client_options].symbolize_keys + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb new file mode 100644 index 00000000000..42c93707caa --- /dev/null +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -0,0 +1,52 @@ +module Gitlab + module BitbucketImport + class Importer + attr_reader :project, :client + + def initialize(project) + @project = project + @client = Client.new(project.creator.bitbucket_access_token, project.creator.bitbucket_access_token_secret) + @formatter = Gitlab::ImportFormatter.new + end + + def execute + project_identifier = project.import_source + + return true unless client.project(project_identifier)["has_issues"] + + #Issues && Comments + issues = client.issues(project_identifier) + + issues["issues"].each do |issue| + body = @formatter.author_line(issue["reported_by"]["username"], issue["content"]) + + comments = client.issue_comments(project_identifier, issue["local_id"]) + + if comments.any? + body += @formatter.comments_header + end + + comments.each do |comment| + body += @formatter.comment(comment["author_info"]["username"], comment["utc_created_on"], comment["content"]) + end + + project.issues.create!( + description: body, + title: issue["title"], + state: %w(resolved invalid duplicate wontfix).include?(issue["status"]) ? 'closed' : 'opened', + author_id: gl_user_id(project, issue["reported_by"]["username"]) + ) + end + + true + end + + private + + def gl_user_id(project, bitbucket_id) + user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s) + (user && user.id) || project.creator_id + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb new file mode 100644 index 00000000000..9931aa7e029 --- /dev/null +++ b/lib/gitlab/bitbucket_import/key_adder.rb @@ -0,0 +1,23 @@ +module Gitlab + module BitbucketImport + class KeyAdder + attr_reader :repo, :current_user, :client + + def initialize(repo, current_user) + @repo, @current_user = repo, current_user + @client = Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret) + end + + def execute + return false unless BitbucketImport.public_key.present? + + project_identifier = "#{repo["owner"]}/#{repo["slug"]}" + client.add_deploy_key(project_identifier, BitbucketImport.public_key) + + true + rescue + false + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/key_deleter.rb b/lib/gitlab/bitbucket_import/key_deleter.rb new file mode 100644 index 00000000000..1a24a86fc37 --- /dev/null +++ b/lib/gitlab/bitbucket_import/key_deleter.rb @@ -0,0 +1,23 @@ +module Gitlab + module BitbucketImport + class KeyDeleter + attr_reader :project, :current_user, :client + + def initialize(project) + @project = project + @current_user = project.creator + @client = Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret) + end + + def execute + return false unless BitbucketImport.public_key.present? + + client.delete_deploy_key(project.import_source, BitbucketImport.public_key) + + true + rescue + false + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb new file mode 100644 index 00000000000..54420e62c90 --- /dev/null +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -0,0 +1,26 @@ +module Gitlab + module BitbucketImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user + + def initialize(repo, namespace, current_user) + @repo = repo + @namespace = namespace + @current_user = current_user + end + + def execute + ::Projects::CreateService.new(current_user, + name: repo["name"], + path: repo["slug"], + description: repo["description"], + namespace_id: namespace.id, + visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, + import_type: "bitbucket", + import_source: "#{repo["owner"]}/#{repo["slug"]}", + import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git" + ).execute + end + end + end +end diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index 401e6e047b1..aeec595782c 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -1,16 +1,20 @@ module Gitlab - module ClosingIssueExtractor + class ClosingIssueExtractor ISSUE_CLOSING_REGEX = Regexp.new(Gitlab.config.gitlab.issue_closing_pattern) - def self.closed_by_message_in_project(message, project) - md = ISSUE_CLOSING_REGEX.match(message) - if md - extractor = Gitlab::ReferenceExtractor.new - extractor.analyze(md[0], project) - extractor.issues_for(project) - else - [] - end + def initialize(project, current_user = nil) + @extractor = Gitlab::ReferenceExtractor.new(project, current_user) + end + + def closed_by_message(message) + return [] if message.nil? + + closing_statements = message.scan(ISSUE_CLOSING_REGEX). + map { |ref| ref[0] }.join(" ") + + @extractor.analyze(closing_statements) + + @extractor.issues end end end diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb new file mode 100644 index 00000000000..45bb904ed7a --- /dev/null +++ b/lib/gitlab/contributions_calendar.rb @@ -0,0 +1,56 @@ +module Gitlab + class ContributionsCalendar + attr_reader :timestamps, :projects, :user + + def initialize(projects, user) + @projects = projects + @user = user + end + + def timestamps + return @timestamps if @timestamps.present? + + @timestamps = {} + date_from = 1.year.ago + date_to = Date.today + + events = Event.reorder(nil).contributions.where(author_id: user.id). + where("created_at > ?", date_from).where(project_id: projects). + group('date(created_at)'). + 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.each do |date| + date_id = date.to_time.to_i.to_s + @timestamps[date_id] = 0 + day_events = events.find { |day_events| day_events["date"] == date } + + if day_events + @timestamps[date_id] = day_events["total_amount"] + end + end + + @timestamps + end + + def events_by_date(date) + events = Event.contributions.where(author_id: user.id). + where("created_at > ? AND created_at < ?", date.beginning_of_day, date.end_of_day). + where(project_id: projects) + + events.select do |event| + event.push? || event.issue? || event.merge_request? + end + end + + def starting_year + (Time.now - 1.year).strftime("%Y") + end + + def starting_month + Date.today.strftime("%m").to_i + end + end +end diff --git a/lib/gitlab/contributors.rb b/lib/gitlab/contributor.rb index c41e92b620f..c41e92b620f 100644 --- a/lib/gitlab/contributors.rb +++ b/lib/gitlab/contributor.rb diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb new file mode 100644 index 00000000000..931d51c55d3 --- /dev/null +++ b/lib/gitlab/current_settings.rb @@ -0,0 +1,29 @@ +module Gitlab + module CurrentSettings + def current_application_settings + key = :current_application_settings + + RequestStore.store[key] ||= begin + if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('application_settings') + ApplicationSetting.current || ApplicationSetting.create_from_defaults + else + fake_application_settings + end + end + end + + def fake_application_settings + OpenStruct.new( + default_projects_limit: Settings.gitlab['default_projects_limit'], + default_branch_protection: Settings.gitlab['default_branch_protection'], + 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'], + restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], + max_attachment_size: Settings.gitlab['max_attachment_size'], + session_expire_delay: Settings.gitlab['session_expire_delay'] + ) + end + end +end diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index f7c1f20d762..c1d9520ddf1 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -4,7 +4,7 @@ module Gitlab include Enumerable def parse(lines) - @lines = lines, + @lines = lines lines_obj = [] line_obj_index = 0 line_old = 1 @@ -27,7 +27,7 @@ module Gitlab 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 lines_obj << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 next @@ -74,7 +74,7 @@ module Gitlab def html_escape(str) replacements = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } - str.gsub(/[&"'><]/, replacements) + str.gsub(/[&"'><]/, replacements) end end end diff --git a/lib/gitlab/force_push_check.rb b/lib/gitlab/force_push_check.rb index 6a52cdba608..fdb6a35c78d 100644 --- a/lib/gitlab/force_push_check.rb +++ b/lib/gitlab/force_push_check.rb @@ -3,13 +3,13 @@ module Gitlab def self.force_push?(project, oldrev, newrev) return false if project.empty_repo? - if oldrev != Gitlab::Git::BLANK_SHA && newrev != Gitlab::Git::BLANK_SHA - missed_refs = IO.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev})).read - missed_refs.split("\n").size > 0 - else + # Created or deleted branch + if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev) false + else + missed_refs, _ = Gitlab::Popen.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev})) + missed_refs.split("\n").size > 0 end end end end - diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 67aca5e36e9..0c350d7c675 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -1,5 +1,25 @@ module Gitlab module Git BLANK_SHA = '0' * 40 + TAG_REF_PREFIX = "refs/tags/" + BRANCH_REF_PREFIX = "refs/heads/" + + class << self + def ref_name(ref) + ref.gsub(/\Arefs\/(tags|heads)\//, '') + end + + def tag_ref?(ref) + ref.start_with?(TAG_REF_PREFIX) + end + + def branch_ref?(ref) + ref.start_with?(BRANCH_REF_PREFIX) + end + + def blank_ref?(ref) + ref == BLANK_SHA + end + end end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 8b4729896b5..c90184d31cf 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -3,110 +3,190 @@ module Gitlab DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive } PUSH_COMMANDS = %w{ git-receive-pack } - attr_reader :params, :project, :git_cmd, :user + attr_reader :actor, :project + + def initialize(actor, project) + @actor = actor + @project = project + end + + def user + return @user if defined?(@user) + + @user = + case actor + when User + actor + when DeployKey + nil + when Key + actor.user + end + end + + def deploy_key + actor if actor.is_a?(DeployKey) + end + + def can_push_to_branch?(ref) + return false unless user + + if project.protected_branch?(ref) && !project.developers_can_push_to_protected_branch?(ref) + user.can?(:push_code_to_protected_branches, project) + else + user.can?(:push_code, project) + end + end + + def can_read_project? + if user + user.can?(:read_project, project) + elsif deploy_key + deploy_key.projects.include?(project) + else + false + end + end + + def check(cmd, changes = nil) + unless actor + return build_status_object(false, "No user or key was provided.") + end + + if user && !user_allowed? + return build_status_object(false, "Your account has been blocked.") + end + + unless project && can_read_project? + return build_status_object(false, 'The project you were looking for could not be found.') + end - def check(actor, cmd, project, changes = nil) case cmd when *DOWNLOAD_COMMANDS - download_access_check(actor, project) + download_access_check when *PUSH_COMMANDS - if actor.is_a? User - push_access_check(actor, project, changes) - elsif actor.is_a? DeployKey - return build_status_object(false, "Deploy key not allowed to push") - elsif actor.is_a? Key - push_access_check(actor.user, project, changes) - else - raise 'Wrong actor' - end + push_access_check(changes) else - return build_status_object(false, "Wrong command") + build_status_object(false, "The command you're trying to execute is not allowed.") end end - def download_access_check(actor, project) - if actor.is_a?(User) - user_download_access_check(actor, project) - elsif actor.is_a?(DeployKey) - if actor.projects.include?(project) - build_status_object(true) - else - build_status_object(false, "Deploy key not allowed to access this project") - end - elsif actor.is_a? Key - user_download_access_check(actor.user, project) + def download_access_check + if user + user_download_access_check + elsif deploy_key + build_status_object(true) else raise 'Wrong actor' end end - def user_download_access_check(user, project) - if user && user_allowed?(user) && user.can?(:download_code, project) - build_status_object(true) + def push_access_check(changes) + if user + user_push_access_check(changes) + elsif deploy_key + build_status_object(false, "Deploy keys are not allowed to push code.") else - build_status_object(false, "You don't have access") + raise 'Wrong actor' + end + end + + def user_download_access_check + unless user.can?(:download_code, project) + return build_status_object(false, "You are not allowed to download code from this project.") end + + build_status_object(true) end - def push_access_check(user, project, changes) - return build_status_object(false, "You don't have access") unless user && user_allowed?(user) - return build_status_object(true) if changes.blank? + def user_push_access_check(changes) + if changes.blank? + return build_status_object(true) + end + + unless project.repository.exists? + return build_status_object(false, "A repository for this project does not exist yet.") + end changes = changes.lines if changes.kind_of?(String) # Iterate over all changes to find if user allowed all of them to be applied - changes.each do |change| - status = change_access_check(user, project, change) + changes.map(&:strip).reject(&:blank?).each do |change| + status = change_access_check(change) unless status.allowed? # If user does not have access to make at least one change - cancel all push return status end end - return build_status_object(true) + build_status_object(true) end - def change_access_check(user, project, change) + def change_access_check(change) oldrev, newrev, ref = change.split(' ') - action = if project.protected_branch?(branch_name(ref)) - # we dont allow force push to protected branch - if forced_push?(project, oldrev, newrev) - :force_push_code_to_protected_branches - # and we dont allow remove of protected branch - elsif newrev == Gitlab::Git::BLANK_SHA - :remove_protected_branches - else - :push_code_to_protected_branches - end - elsif project.repository && project.repository.tag_names.include?(tag_name(ref)) - # Prevent any changes to existing git tag unless user has permissions - :admin_project - else - :push_code - end - - if user.can?(action, project) - build_status_object(true) - else - build_status_object(false, "You don't have permission") + action = + if project.protected_branch?(branch_name(ref)) + protected_branch_action(oldrev, newrev, branch_name(ref)) + elsif protected_tag?(tag_name(ref)) + # Prevent any changes to existing git tag unless user has permissions + :admin_project + else + :push_code + end + + unless user.can?(action, project) + status = + case action + when :force_push_code_to_protected_branches + build_status_object(false, "You are not allowed to force push code to a protected branch on this project.") + when :remove_protected_branches + build_status_object(false, "You are not allowed to deleted protected branches from this project.") + when :push_code_to_protected_branches + build_status_object(false, "You are not allowed to push code to protected branches on this project.") + when :admin_project + build_status_object(false, "You are not allowed to change existing tags on this project.") + else # :push_code + build_status_object(false, "You are not allowed to push code to this project.") + end + return status end + + build_status_object(true) end - def forced_push?(project, oldrev, newrev) + def forced_push?(oldrev, newrev) Gitlab::ForcePushCheck.force_push?(project, oldrev, newrev) end private - def user_allowed?(user) + def protected_branch_action(oldrev, newrev, branch_name) + # we dont allow force push to protected branch + if forced_push?(oldrev, newrev) + :force_push_code_to_protected_branches + elsif Gitlab::Git.blank_ref?(newrev) + # and we dont allow remove of protected branch + :remove_protected_branches + elsif project.developers_can_push_to_protected_branch?(branch_name) + :push_code + else + :push_code_to_protected_branches + end + end + + def protected_tag?(tag_name) + project.repository.tag_names.include?(tag_name) + end + + def user_allowed? Gitlab::UserAccess.allowed?(user) end def branch_name(ref) ref = ref.to_s - if ref.start_with?('refs/heads') - ref.sub(%r{\Arefs/heads/}, '') + if Gitlab::Git.branch_ref?(ref) + Gitlab::Git.ref_name(ref) else nil end @@ -114,8 +194,8 @@ module Gitlab def tag_name(ref) ref = ref.to_s - if ref.start_with?('refs/tags') - ref.sub(%r{\Arefs/tags/}, '') + if Gitlab::Git.tag_ref?(ref) + Gitlab::Git.ref_name(ref) else nil end diff --git a/lib/gitlab/git_access_status.rb b/lib/gitlab/git_access_status.rb index 3d451ecebee..5a806ff6e0d 100644 --- a/lib/gitlab/git_access_status.rb +++ b/lib/gitlab/git_access_status.rb @@ -9,7 +9,7 @@ module Gitlab end def to_json - {status: @status, message: @message}.to_json + { status: @status, message: @message }.to_json end end -end
\ No newline at end of file +end diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb index a2177c8d548..8ba97184e69 100644 --- a/lib/gitlab/git_access_wiki.rb +++ b/lib/gitlab/git_access_wiki.rb @@ -1,10 +1,10 @@ module Gitlab class GitAccessWiki < GitAccess - def change_access_check(user, project, change) + def change_access_check(change) if user.can?(:write_wiki, project) build_status_object(true) else - build_status_object(false, "You don't have access") + build_status_object(false, "You are not allowed to write to this project's wiki.") end end end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb new file mode 100644 index 00000000000..270cbcd9ccd --- /dev/null +++ b/lib/gitlab/github_import/client.rb @@ -0,0 +1,53 @@ +module Gitlab + module GithubImport + class Client + attr_reader :client, :api + + def initialize(access_token) + @client = ::OAuth2::Client.new( + config.app_id, + config.app_secret, + github_options + ) + + if access_token + ::Octokit.auto_paginate = true + @api = ::Octokit::Client.new(access_token: access_token) + end + end + + def authorize_url(redirect_uri) + client.auth_code.authorize_url({ + redirect_uri: redirect_uri, + scope: "repo, user, user:email" + }) + end + + def get_token(code) + client.auth_code.get_token(code).token + end + + def method_missing(method, *args, &block) + if api.respond_to?(method) + api.send(method, *args, &block) + else + super(method, *args, &block) + end + end + + def respond_to?(method) + api.respond_to?(method) || super + end + + private + + def config + Gitlab.config.omniauth.providers.find{|provider| provider.name == "github"} + end + + def github_options + OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys + end + end + end +end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb new file mode 100644 index 00000000000..23832b3233c --- /dev/null +++ b/lib/gitlab/github_import/importer.rb @@ -0,0 +1,46 @@ +module Gitlab + module GithubImport + class Importer + attr_reader :project, :client + + def initialize(project) + @project = project + @client = Client.new(project.creator.github_access_token) + @formatter = Gitlab::ImportFormatter.new + end + + def execute + #Issues && Comments + client.list_issues(project.import_source, state: :all).each do |issue| + if issue.pull_request.nil? + + body = @formatter.author_line(issue.user.login, issue.body) + + if issue.comments > 0 + body += @formatter.comments_header + + client.issue_comments(project.import_source, issue.number).each do |c| + body += @formatter.comment(c.user.login, c.created_at, c.body) + end + end + + project.issues.create!( + description: body, + title: issue.title, + state: issue.state == 'closed' ? 'closed' : 'opened', + author_id: gl_user_id(project, issue.user.id) + ) + end + end + end + + private + + def gl_user_id(project, github_id) + user = User.joins(:identities). + find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s) + (user && user.id) || project.creator_id + end + end + end +end diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb new file mode 100644 index 00000000000..2723eec933e --- /dev/null +++ b/lib/gitlab/github_import/project_creator.rb @@ -0,0 +1,26 @@ +module Gitlab + module GithubImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user + + def initialize(repo, namespace, current_user) + @repo = repo + @namespace = namespace + @current_user = current_user + end + + def execute + ::Projects::CreateService.new(current_user, + name: repo.name, + path: repo.name, + description: repo.description, + namespace_id: namespace.id, + visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, + import_type: "github", + import_source: repo.full_name, + import_url: repo.clone_url.sub("https://", "https://#{current_user.github_access_token}@") + ).execute + end + end + end +end diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb new file mode 100644 index 00000000000..9c00896c913 --- /dev/null +++ b/lib/gitlab/gitlab_import/client.rb @@ -0,0 +1,82 @@ +module Gitlab + module GitlabImport + class Client + attr_reader :client, :api + + PER_PAGE = 100 + + def initialize(access_token) + @client = ::OAuth2::Client.new( + config.app_id, + config.app_secret, + gitlab_options + ) + + if access_token + @api = OAuth2::AccessToken.from_hash(@client, access_token: access_token) + end + end + + def authorize_url(redirect_uri) + client.auth_code.authorize_url({ + redirect_uri: redirect_uri, + scope: "api" + }) + end + + def get_token(code, redirect_uri) + client.auth_code.get_token(code, redirect_uri: redirect_uri).token + end + + def user + api.get("/api/v3/user").parsed + end + + def issues(project_identifier) + lazy_page_iterator(PER_PAGE) do |page| + api.get("/api/v3/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed + end + end + + def issue_comments(project_identifier, issue_id) + lazy_page_iterator(PER_PAGE) do |page| + api.get("/api/v3/projects/#{project_identifier}/issues/#{issue_id}/notes?per_page=#{PER_PAGE}&page=#{page}").parsed + end + end + + def project(id) + api.get("/api/v3/projects/#{id}").parsed + end + + def projects + lazy_page_iterator(PER_PAGE) do |page| + api.get("/api/v3/projects?per_page=#{PER_PAGE}&page=#{page}").parsed + end + end + + private + + def lazy_page_iterator(per_page) + Enumerator.new do |y| + page = 1 + loop do + items = yield(page) + items.each do |item| + y << item + end + break if items.empty? || items.size < per_page + page += 1 + end + end + end + + def config + Gitlab.config.omniauth.providers.find{|provider| provider.name == "gitlab"} + end + + def gitlab_options + OmniAuth::Strategies::GitLab.default_options[:client_options].symbolize_keys + end + end + end +end diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb new file mode 100644 index 00000000000..c5304a0699b --- /dev/null +++ b/lib/gitlab/gitlab_import/importer.rb @@ -0,0 +1,50 @@ +module Gitlab + module GitlabImport + class Importer + attr_reader :project, :client + + def initialize(project) + @project = project + @client = Client.new(project.creator.gitlab_access_token) + @formatter = Gitlab::ImportFormatter.new + end + + def execute + project_identifier = URI.encode(project.import_source, '/') + + #Issues && Comments + issues = client.issues(project_identifier) + + issues.each do |issue| + body = @formatter.author_line(issue["author"]["name"], issue["description"]) + + comments = client.issue_comments(project_identifier, issue["id"]) + + if comments.any? + body += @formatter.comments_header + end + + comments.each do |comment| + body += @formatter.comment(comment["author"]["name"], comment["created_at"], comment["body"]) + end + + project.issues.create!( + description: body, + title: issue["title"], + state: issue["state"], + author_id: gl_user_id(project, issue["author"]["id"]) + ) + end + + true + end + + private + + def gl_user_id(project, gitlab_id) + user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'gitlab'", gitlab_id.to_s) + (user && user.id) || project.creator_id + end + end + end +end diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb new file mode 100644 index 00000000000..f0d7141bf56 --- /dev/null +++ b/lib/gitlab/gitlab_import/project_creator.rb @@ -0,0 +1,26 @@ +module Gitlab + module GitlabImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user + + def initialize(repo, namespace, current_user) + @repo = repo + @namespace = namespace + @current_user = current_user + end + + def execute + ::Projects::CreateService.new(current_user, + name: repo["name"], + path: repo["path"], + description: repo["description"], + namespace_id: namespace.id, + visibility_level: repo["visibility_level"], + import_type: "gitlab", + import_source: repo["path_with_namespace"], + import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{current_user.gitlab_access_token}@") + ).execute + end + end + end +end diff --git a/lib/gitlab/gitorious_import.rb b/lib/gitlab/gitorious_import.rb new file mode 100644 index 00000000000..8d0132a744c --- /dev/null +++ b/lib/gitlab/gitorious_import.rb @@ -0,0 +1,5 @@ +module Gitlab + module GitoriousImport + GITORIOUS_HOST = "https://gitorious.org" + end +end diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb new file mode 100644 index 00000000000..99fe5bdebfc --- /dev/null +++ b/lib/gitlab/gitorious_import/client.rb @@ -0,0 +1,29 @@ +module Gitlab + module GitoriousImport + class Client + attr_reader :repo_list + + def initialize(repo_list) + @repo_list = repo_list + end + + def authorize_url(redirect_uri) + "#{GITORIOUS_HOST}/gitlab-import?callback_url=#{redirect_uri}" + end + + def repos + @repos ||= repo_names.map { |full_name| GitoriousImport::Repository.new(full_name) } + end + + def repo(id) + repos.find { |repo| repo.id == id } + end + + private + + def repo_names + repo_list.to_s.split(',').map(&:strip).reject(&:blank?) + end + end + end +end diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb new file mode 100644 index 00000000000..cc9a91c91f4 --- /dev/null +++ b/lib/gitlab/gitorious_import/project_creator.rb @@ -0,0 +1,26 @@ +module Gitlab + module GitoriousImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user + + def initialize(repo, namespace, current_user) + @repo = repo + @namespace = namespace + @current_user = current_user + end + + def execute + ::Projects::CreateService.new(current_user, + name: repo.name, + path: repo.path, + description: repo.description, + namespace_id: namespace.id, + visibility_level: Gitlab::VisibilityLevel::PUBLIC, + import_type: "gitorious", + import_source: repo.full_name, + import_url: repo.import_url + ).execute + end + end + end +end diff --git a/lib/gitlab/gitorious_import/repository.rb b/lib/gitlab/gitorious_import/repository.rb new file mode 100644 index 00000000000..c88f1ae358d --- /dev/null +++ b/lib/gitlab/gitorious_import/repository.rb @@ -0,0 +1,35 @@ +module Gitlab + module GitoriousImport + Repository = Struct.new(:full_name) do + def id + Digest::SHA1.hexdigest(full_name) + end + + def namespace + segments.first + end + + def path + segments.last + end + + def name + path.titleize + end + + def description + "" + end + + def import_url + "#{GITORIOUS_HOST}/#{full_name}.git" + end + + private + + def segments + full_name.split('/') + end + end + end +end diff --git a/lib/gitlab/google_code_import/client.rb b/lib/gitlab/google_code_import/client.rb new file mode 100644 index 00000000000..890bd9a3554 --- /dev/null +++ b/lib/gitlab/google_code_import/client.rb @@ -0,0 +1,52 @@ +module Gitlab + module GoogleCodeImport + class Client + attr_reader :raw_data + + def self.mask_email(author) + parts = author.split("@", 2) + parts[0] = "#{parts[0][0...-3]}..." + parts.join("@") + end + + def initialize(raw_data) + @raw_data = raw_data + end + + def valid? + raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#user" && raw_data.has_key?("projects") + end + + def repos + @repos ||= raw_data["projects"].map { |raw_repo| GoogleCodeImport::Repository.new(raw_repo) }.select(&:git?) + end + + def incompatible_repos + @incompatible_repos ||= raw_data["projects"].map { |raw_repo| GoogleCodeImport::Repository.new(raw_repo) }.reject(&:git?) + end + + def repo(id) + repos.find { |repo| repo.id == id } + end + + def user_map + user_map = Hash.new { |hash, user| hash[user] = self.class.mask_email(user) } + + repos.each do |repo| + next unless repo.valid? && repo.issues + + repo.issues.each do |raw_issue| + # Touching is enough to add the entry and masked email. + user_map[raw_issue["author"]["name"]] + + raw_issue["comments"]["items"].each do |raw_comment| + user_map[raw_comment["author"]["name"]] + end + end + end + + Hash[user_map.sort] + end + end + end +end diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb new file mode 100644 index 00000000000..70bfe059776 --- /dev/null +++ b/lib/gitlab/google_code_import/importer.rb @@ -0,0 +1,377 @@ +module Gitlab + module GoogleCodeImport + class Importer + attr_reader :project, :repo + + def initialize(project) + @project = project + + import_data = project.import_data.try(:data) + repo_data = import_data["repo"] if import_data + @repo = GoogleCodeImport::Repository.new(repo_data) + + @closed_statuses = [] + @known_labels = Set.new + end + + def execute + return true unless repo.valid? + + import_status_labels + + import_labels + + import_issues + + true + end + + private + + def user_map + @user_map ||= begin + user_map = Hash.new do |hash, user| + # Replace ... by \.\.\., so `johnsm...@gmail.com` isn't autolinked. + Client.mask_email(user).sub("...", "\\.\\.\\.") + end + + import_data = project.import_data.try(:data) + stored_user_map = import_data["user_map"] if import_data + user_map.update(stored_user_map) if stored_user_map + + user_map + end + end + + def import_status_labels + repo.raw_data["issuesConfig"]["statuses"].each do |status| + closed = !status["meansOpen"] + @closed_statuses << status["status"] if closed + + name = nice_status_name(status["status"]) + create_label(name) + @known_labels << name + end + end + + def import_labels + repo.raw_data["issuesConfig"]["labels"].each do |label| + name = nice_label_name(label["label"]) + create_label(name) + @known_labels << name + end + end + + def import_issues + return unless repo.issues + + while raw_issue = repo.issues.shift + author = user_map[raw_issue["author"]["name"]] + date = DateTime.parse(raw_issue["published"]).to_formatted_s(:long) + + comments = raw_issue["comments"]["items"] + issue_comment = comments.shift + + content = format_content(issue_comment["content"]) + attachments = format_attachments(raw_issue["id"], 0, issue_comment["attachments"]) + + body = format_issue_body(author, date, content, attachments) + + labels = [] + raw_issue["labels"].each do |label| + name = nice_label_name(label) + labels << name + + unless @known_labels.include?(name) + create_label(name) + @known_labels << name + end + end + labels << nice_status_name(raw_issue["status"]) + + assignee_id = nil + if raw_issue.has_key?("owner") + username = user_map[raw_issue["owner"]["name"]] + + if username.start_with?("@") + username = username[1..-1] + + if user = User.find_by(username: username) + assignee_id = user.id + end + end + end + + issue = Issue.create!( + project_id: project.id, + title: raw_issue["title"], + description: body, + author_id: project.creator_id, + assignee_id: assignee_id, + state: raw_issue["state"] == "closed" ? "closed" : "opened" + ) + issue.add_labels_by_names(labels) + + if issue.iid != raw_issue["id"] + issue.update_attribute(:iid, raw_issue["id"]) + end + + import_issue_comments(issue, comments) + end + end + + def import_issue_comments(issue, comments) + Note.transaction do + while raw_comment = comments.shift + next if raw_comment.has_key?("deletedBy") + + content = format_content(raw_comment["content"]) + updates = format_updates(raw_comment["updates"]) + attachments = format_attachments(issue.iid, raw_comment["id"], raw_comment["attachments"]) + + next if content.blank? && updates.blank? && attachments.blank? + + author = user_map[raw_comment["author"]["name"]] + date = DateTime.parse(raw_comment["published"]).to_formatted_s(:long) + + body = format_issue_comment_body( + raw_comment["id"], + author, + date, + content, + updates, + attachments + ) + + # Needs to match order of `comment_columns` below. + Note.create!( + project_id: project.id, + noteable_type: "Issue", + noteable_id: issue.id, + author_id: project.creator_id, + note: body + ) + end + end + end + + def nice_label_color(name) + case name + when /\AComponent:/ + "#fff39e" + when /\AOpSys:/ + "#e2e2e2" + when /\AMilestone:/ + "#fee3ff" + + when *@closed_statuses.map { |s| nice_status_name(s) } + "#cfcfcf" + when "Status: New" + "#428bca" + when "Status: Accepted" + "#5cb85c" + when "Status: Started" + "#8e44ad" + + when "Priority: Critical" + "#ffcfcf" + when "Priority: High" + "#deffcf" + when "Priority: Medium" + "#fff5cc" + when "Priority: Low" + "#cfe9ff" + + when "Type: Defect" + "#d9534f" + when "Type: Enhancement" + "#44ad8e" + when "Type: Task" + "#4b6dd0" + when "Type: Review" + "#8e44ad" + when "Type: Other" + "#7f8c8d" + else + "#e2e2e2" + end + end + + def nice_label_name(name) + name.sub("-", ": ") + end + + def nice_status_name(name) + "Status: #{name}" + end + + def linkify_issues(s) + s = s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2') + s = s.gsub(/([Cc]omment) #([0-9]+)/, '\1 \2') + s + end + + def escape_for_markdown(s) + # No headings and lists + s = s.gsub(/^#/, "\\#") + s = s.gsub(/^-/, "\\-") + + # No inline code + s = s.gsub("`", "\\`") + + # Carriage returns make me sad + s = s.gsub("\r", "") + + # Markdown ignores single newlines, but we need them as <br />. + s = s.gsub("\n", " \n") + + s + end + + def create_label(name) + color = nice_label_color(name) + Label.create!(project_id: project.id, name: name, color: color) + end + + def format_content(raw_content) + linkify_issues(escape_for_markdown(raw_content)) + end + + def format_updates(raw_updates) + updates = [] + + if raw_updates.has_key?("status") + updates << "*Status: #{raw_updates["status"]}*" + end + + if raw_updates.has_key?("owner") + updates << "*Owner: #{user_map[raw_updates["owner"]]}*" + end + + if raw_updates.has_key?("cc") + cc = raw_updates["cc"].map do |l| + deleted = l.start_with?("-") + l = l[1..-1] if deleted + l = user_map[l] + l = "~~#{l}~~" if deleted + l + end + + updates << "*Cc: #{cc.join(", ")}*" + end + + if raw_updates.has_key?("labels") + labels = raw_updates["labels"].map do |l| + deleted = l.start_with?("-") + l = l[1..-1] if deleted + l = nice_label_name(l) + l = "~~#{l}~~" if deleted + l + end + + updates << "*Labels: #{labels.join(", ")}*" + end + + if raw_updates.has_key?("mergedInto") + updates << "*Merged into: ##{raw_updates["mergedInto"]}*" + end + + if raw_updates.has_key?("blockedOn") + blocked_ons = raw_updates["blockedOn"].map do |raw_blocked_on| + name, id = raw_blocked_on.split(":", 2) + + deleted = name.start_with?("-") + name = name[1..-1] if deleted + + text = + if name == project.import_source + "##{id}" + else + "#{project.namespace.path}/#{name}##{id}" + end + text = "~~#{text}~~" if deleted + text + end + updates << "*Blocked on: #{blocked_ons.join(", ")}*" + end + + if raw_updates.has_key?("blocking") + blockings = raw_updates["blocking"].map do |raw_blocked_on| + name, id = raw_blocked_on.split(":", 2) + + deleted = name.start_with?("-") + name = name[1..-1] if deleted + + text = + if name == project.import_source + "##{id}" + else + "#{project.namespace.path}/#{name}##{id}" + end + text = "~~#{text}~~" if deleted + text + end + updates << "*Blocking: #{blockings.join(", ")}*" + end + + updates + end + + def format_attachments(issue_id, comment_id, raw_attachments) + return [] unless raw_attachments + + raw_attachments.map do |attachment| + next if attachment["isDeleted"] + + filename = attachment["fileName"] + link = "https://storage.googleapis.com/google-code-attachments/#{@repo.name}/issue-#{issue_id}/comment-#{comment_id}/#{filename}" + + text = "[#{filename}](#{link})" + text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/ + text + end.compact + end + + def format_issue_comment_body(id, author, date, content, updates, attachments) + body = [] + body << "*Comment #{id} by #{author} on #{date}*" + body << "---" + + if content.blank? + content = "*(No comment has been entered for this change)*" + end + body << content + + if updates.any? + body << "---" + body += updates + end + + if attachments.any? + body << "---" + body += attachments + end + + body.join("\n\n") + end + + def format_issue_body(author, date, content, attachments) + body = [] + body << "*By #{author} on #{date} (imported from Google Code)*" + body << "---" + + if content.blank? + content = "*(No description has been entered for this issue)*" + end + body << content + + if attachments.any? + body << "---" + body += attachments + end + + body.join("\n\n") + end + end + end +end diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb new file mode 100644 index 00000000000..0cfeaf9d61c --- /dev/null +++ b/lib/gitlab/google_code_import/project_creator.rb @@ -0,0 +1,37 @@ +module Gitlab + module GoogleCodeImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user, :user_map + + def initialize(repo, namespace, current_user, user_map = nil) + @repo = repo + @namespace = namespace + @current_user = current_user + @user_map = user_map + end + + def execute + project = ::Projects::CreateService.new(current_user, + name: repo.name, + path: repo.name, + description: repo.summary, + namespace: namespace, + creator: current_user, + visibility_level: Gitlab::VisibilityLevel::PUBLIC, + import_type: "google_code", + import_source: repo.name, + import_url: repo.import_url + ).execute + + import_data = project.create_import_data( + data: { + "repo" => repo.raw_data, + "user_map" => user_map + } + ) + + project + end + end + end +end diff --git a/lib/gitlab/google_code_import/repository.rb b/lib/gitlab/google_code_import/repository.rb new file mode 100644 index 00000000000..ad33fc2cad2 --- /dev/null +++ b/lib/gitlab/google_code_import/repository.rb @@ -0,0 +1,43 @@ +module Gitlab + module GoogleCodeImport + class Repository + attr_accessor :raw_data + + def initialize(raw_data) + @raw_data = raw_data + end + + def valid? + raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#project" + end + + def id + raw_data["externalId"] + end + + def name + raw_data["name"] + end + + def summary + raw_data["summary"] + end + + def description + raw_data["description"] + end + + def git? + raw_data["versionControlSystem"] == "git" + end + + def import_url + raw_data["repositoryUrls"].first + end + + def issues + raw_data["issues"] && raw_data["issues"]["items"] + end + end + end +end diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb index 6e4de197eeb..3e5d728f3bc 100644 --- a/lib/gitlab/identifier.rb +++ b/lib/gitlab/identifier.rb @@ -5,7 +5,7 @@ module Gitlab def identify(identifier, project, newrev) if identifier.blank? # Local push from gitlab - email = project.repository.commit(newrev).author_email rescue nil + email = project.commit(newrev).author_email rescue nil User.find_by(email: email) if email elsif identifier =~ /\Auser-\d+\Z/ diff --git a/lib/gitlab/import_formatter.rb b/lib/gitlab/import_formatter.rb new file mode 100644 index 00000000000..72e041a90b1 --- /dev/null +++ b/lib/gitlab/import_formatter.rb @@ -0,0 +1,15 @@ +module Gitlab + class ImportFormatter + def comment(author, date, body) + "\n\n*By #{author} on #{date}*\n\n#{body}" + end + + def comments_header + "\n\n\n**Imported comments:**\n" + end + + def author_line(author, body) + "*Created by: #{author}*\n\n#{body}" + end + end +end diff --git a/lib/gitlab/key_fingerprint.rb b/lib/gitlab/key_fingerprint.rb new file mode 100644 index 00000000000..baf52ff750d --- /dev/null +++ b/lib/gitlab/key_fingerprint.rb @@ -0,0 +1,55 @@ +module Gitlab + class KeyFingerprint + include Gitlab::Popen + + attr_accessor :key + + def initialize(key) + @key = key + end + + def fingerprint + cmd_status = 0 + cmd_output = '' + + Tempfile.open('gitlab_key_file') do |file| + file.puts key + file.rewind + + cmd = [] + cmd.push *%W(ssh-keygen) + cmd.push *%W(-E md5) if explicit_fingerprint_algorithm? + cmd.push *%W(-lf #{file.path}) + + cmd_output, cmd_status = popen(cmd, '/tmp') + end + + return nil unless cmd_status.zero? + + # 16 hex bytes separated by ':', optionally starting with "MD5:" + fingerprint_matches = cmd_output.match(/(MD5:)?(?<fingerprint>(\h{2}:){15}\h{2})/) + return nil unless fingerprint_matches + + fingerprint_matches[:fingerprint] + end + + private + + def explicit_fingerprint_algorithm? + # OpenSSH 6.8 introduces a new default output format for fingerprints. + # Check the version and decide which command to use. + + version_output, version_status = popen(%W(ssh -V)) + return false unless version_status.zero? + + version_matches = version_output.match(/OpenSSH_(?<major>\d+)\.(?<minor>\d+)/) + return false unless version_matches + + version_info = Gitlab::VersionInfo.new(version_matches[:major].to_i, version_matches[:minor].to_i) + + required_version_info = Gitlab::VersionInfo.new(6, 8) + + version_info >= required_version_info + end + end +end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index eb2c4e48ff2..16ff03c38d4 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -8,7 +8,7 @@ module Gitlab attr_reader :adapter, :provider, :user def self.open(user, &block) - Gitlab::LDAP::Adapter.open(user.provider) do |adapter| + Gitlab::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter| block.call(self.new(user, adapter)) end end @@ -28,13 +28,21 @@ module Gitlab def initialize(user, adapter=nil) @adapter = adapter @user = user - @provider = user.provider + @provider = user.ldap_identity.provider end def allowed? - if Gitlab::LDAP::Person.find_by_dn(user.extern_uid, adapter) + if Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter) return true unless ldap_config.active_directory - !Gitlab::LDAP::Person.disabled_via_active_directory?(user.extern_uid, adapter) + + # Block user in GitLab if he/she was blocked in AD + if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) + user.block unless user.blocked? + false + else + user.activate if user.blocked? && !ldap_config.block_auto_created_users + true + end else false end diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index 256cdb4c2f1..577a890a7d9 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -63,8 +63,10 @@ module Gitlab end def dn_matches_filter?(dn, filter) - ldap_search(base: dn, filter: filter, - scope: Net::LDAP::SearchScope_BaseObject, attributes: %w{dn}).any? + ldap_search(base: dn, + filter: filter, + scope: Net::LDAP::SearchScope_BaseObject, + attributes: %w{dn}).any? end def ldap_search(*args) diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb index 8af2c74e959..bad683c6511 100644 --- a/lib/gitlab/ldap/authentication.rb +++ b/lib/gitlab/ldap/authentication.rb @@ -1,4 +1,4 @@ -# This calls helps to authenticate to LDAP by providing username and password +# These calls help to authenticate to LDAP by providing username and password # # Since multiple LDAP servers are supported, it will loop through all of them # until a valid bind is found @@ -50,7 +50,7 @@ module Gitlab end def user_filter(login) - filter = Net::LDAP::Filter.eq(config.uid, login) + filter = Net::LDAP::Filter.equals(config.uid, login) # Apply LDAP user filter if present if config.user_filter.present? diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index 0cb24d0ccc1..d2ffa2e1fe8 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -27,8 +27,6 @@ module Gitlab def initialize(provider) if self.class.valid_provider?(provider) @provider = provider - elsif provider == 'ldap' - @provider = self.class.providers.first else self.class.invalid_provider(provider) end @@ -82,6 +80,10 @@ module Gitlab options['active_directory'] end + def block_auto_created_users + options['block_auto_created_users'] + end + protected def base_config Gitlab.config.ldap diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 3e0b3e6cbf8..b81f3e8e8f5 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -9,6 +9,7 @@ module Gitlab attr_accessor :entry, :provider def self.find_by_uid(uid, adapter) + uid = Net::LDAP::Filter.escape(uid) adapter.user(adapter.config.uid, uid) end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 3176e9790a7..f7f3ba9ad7d 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -1,4 +1,4 @@ -require 'gitlab/oauth/user' +require 'gitlab/o_auth/user' # LDAP extension for User model # @@ -12,9 +12,10 @@ module Gitlab class << self def find_by_uid_and_provider(uid, provider) # LDAP distinguished name is case-insensitive - ::User. - where(provider: [provider, :ldap]). + identity = ::Identity. + where(provider: provider). where('lower(extern_uid) = ?', uid.downcase).last + identity && identity.user end end @@ -34,28 +35,37 @@ module Gitlab end def find_by_email - model.find_by(email: auth_hash.email) + ::User.find_by(email: auth_hash.email) end def update_user_attributes - gl_user.attributes = { - extern_uid: auth_hash.uid, - provider: auth_hash.provider, - email: auth_hash.email - } + return unless persisted? + + gl_user.skip_reconfirmation! + gl_user.email = auth_hash.email + + # Build new identity only if we dont have have same one + gl_user.identities.find_or_initialize_by(provider: auth_hash.provider, + extern_uid: auth_hash.uid) + + gl_user end def changed? - gl_user.changed? + gl_user.changed? || gl_user.identities.any?(&:changed?) end - def needs_blocking? - false + def block_after_signup? + ldap_config.block_auto_created_users end def allowed? Gitlab::LDAP::Access.allowed?(gl_user) end + + def ldap_config + Gitlab::LDAP::Config.new(auth_hash.provider) + end end end end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 068c342398b..fa9c0975bb8 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -1,49 +1,45 @@ require 'html/pipeline' -require 'html/pipeline/gitlab' module Gitlab # Custom parser for GitLab-flavored Markdown # - # It replaces references in the text with links to the appropriate items in - # GitLab. - # - # Supported reference formats are: - # * @foo for team members - # * #123 for issues - # * #JIRA-123 for Jira issues - # * !123 for merge requests - # * $123 for snippets - # * 123456 for commits - # - # It also parses Emoji codes to insert images. See - # http://www.emoji-cheat-sheet.com/ for a list of the supported icons. - # - # Examples - # - # >> gfm("Hey @david, can you fix this?") - # => "Hey <a href="/u/david">@david</a>, can you fix this?" - # - # >> gfm("Commit 35d5f7c closes #1234") - # => "Commit <a href="/gitlab/commits/35d5f7c">35d5f7c</a> closes <a href="/gitlab/issues/1234">#1234</a>" - # - # >> gfm(":trollface:") - # => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" /> + # See the files in `lib/gitlab/markdown/` for specific processing information. module Markdown - include IssuesHelper - - attr_reader :html_options + # Provide autoload paths for filters to prevent a circular dependency error + autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter' + autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter' + autoload :CommitReferenceFilter, 'gitlab/markdown/commit_reference_filter' + autoload :EmojiFilter, 'gitlab/markdown/emoji_filter' + autoload :ExternalIssueReferenceFilter, 'gitlab/markdown/external_issue_reference_filter' + autoload :ExternalLinkFilter, 'gitlab/markdown/external_link_filter' + autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter' + autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter' + autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter' + autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' + autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter' + autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter' + autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter' + autoload :TaskListFilter, 'gitlab/markdown/task_list_filter' + autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' - def gfm_with_tasks(text, project = @project, html_options = {}) - text = gfm(text, project, html_options) - parse_tasks(text) + # Public: Parse the provided text with GitLab-Flavored Markdown + # + # text - the source text + # options - options + # html_options - extra options for the reference links as given to link_to + def gfm(text, options = {}, html_options = {}) + gfm_with_options(text, options, html_options) end # Public: Parse the provided text with GitLab-Flavored Markdown # # text - the source text - # project - extra options for the reference links as given to link_to + # options - A Hash of options used to customize output (default: {}): + # :xhtml - output XHTML instead of HTML + # :reference_only_path - Use relative path for reference links + # project - the project # html_options - extra options for the reference links as given to link_to - def gfm(text, project = @project, html_options = {}) + def gfm_with_options(text, options = {}, html_options = {}) return text if text.nil? # Duplicate the string so we don't alter the original, then call to_str @@ -51,251 +47,79 @@ module Gitlab # for gsub calls to work as we need them to. text = text.dup.to_str - @html_options = html_options + options.reverse_merge!( + xhtml: false, + reference_only_path: true, + project: @project, + current_user: current_user + ) - # Extract pre blocks so they are not altered - # from http://github.github.com/github-flavored-markdown/ - text.gsub!(%r{<pre>.*?</pre>|<code>.*?</code>}m) { |match| extract_piece(match) } - # Extract links with probably parsable hrefs - text.gsub!(%r{<a.*?>.*?</a>}m) { |match| extract_piece(match) } - # Extract images with probably parsable src - text.gsub!(%r{<img.*?>}m) { |match| extract_piece(match) } + pipeline = HTML::Pipeline.new(filters) - # TODO: add popups with additional information + context = { + # SanitizationFilter + pipeline: options[:pipeline], - text = parse(text, project) + # EmojiFilter + asset_root: Gitlab.config.gitlab.url, + asset_host: Gitlab::Application.config.asset_host, - # Insert pre block extractions - text.gsub!(/\{gfm-extraction-(\h{32})\}/) do - insert_piece($1) - end + # TableOfContentsFilter + no_header_anchors: options[:no_header_anchors], - # Used markdown pipelines in GitLab: - # GitlabEmojiFilter - performs emoji replacement. - # - # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters - filters = [ - HTML::Pipeline::Gitlab::GitlabEmojiFilter - ] + # ReferenceFilter + current_user: options[:current_user], + only_path: options[:reference_only_path], + project: options[:project], + reference_class: html_options[:class], - markdown_context = { - asset_root: Gitlab.config.gitlab.url, - asset_host: Gitlab::Application.config.asset_host + # RelativeLinkFilter + ref: @ref, + requested_path: @path, + project_wiki: @project_wiki } - markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline - - result = markdown_pipeline.call(text, markdown_context) - text = result[:output].to_html(save_with: 0) - - allowed_attributes = ActionView::Base.sanitized_allowed_attributes - allowed_tags = ActionView::Base.sanitized_allowed_tags - - sanitize text.html_safe, - attributes: allowed_attributes + %w(id class), - tags: allowed_tags + %w(table tr td th) - end - - private - - def extract_piece(text) - @extractions ||= {} - - md5 = Digest::MD5.hexdigest(text) - @extractions[md5] = text - "{gfm-extraction-#{md5}}" - end - - def insert_piece(id) - @extractions[id] - end - - # Private: Parses text for references and emoji - # - # text - Text to parse - # - # Returns parsed text - def parse(text, project = @project) - parse_references(text, project) if project - - text - end - - NAME_STR = '[a-zA-Z][a-zA-Z0-9_\-\.]*' - PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})" - - REFERENCE_PATTERN = %r{ - (?<prefix>\W)? # Prefix - ( # Reference - @(?<user>#{NAME_STR}) # User name - |(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID - |#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID - |#{PROJ_STR}?!(?<merge_request>\d+) # MR ID - |\$(?<snippet>\d+) # Snippet ID - |(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID - |(?<skip>gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit - ) - (?<suffix>\W)? # Suffix - }x.freeze - - TYPES = [:user, :issue, :merge_request, :snippet, :commit].freeze - - def parse_references(text, project = @project) - # parse reference links - text.gsub!(REFERENCE_PATTERN) do |match| - type = TYPES.select{|t| !$~[t].nil?}.first + result = pipeline.call(text, context) - actual_project = project - project_prefix = nil - project_path = $LAST_MATCH_INFO[:project] - if project_path - actual_project = ::Project.find_with_namespace(project_path) - project_prefix = project_path - end - - parse_result($LAST_MATCH_INFO, type, - actual_project, project_prefix) || match + save_options = 0 + if options[:xhtml] + save_options |= Nokogiri::XML::Node::SaveOptions::AS_XHTML end - end - - # Called from #parse_references. Attempts to build a gitlab reference - # link. Returns nil if +type+ is nil, if the match string is an HTML - # entity, if the reference is invalid, or if the matched text includes an - # invalid project path. - def parse_result(match_info, type, project, project_prefix) - prefix = match_info[:prefix] - suffix = match_info[:suffix] - return nil if html_entity?(prefix, suffix) || type.nil? - return nil if project.nil? && !project_prefix.nil? + text = result[:output].to_html(save_with: save_options) - identifier = match_info[type] - ref_link = reference_link(type, identifier, project, project_prefix) - - if ref_link - "#{prefix}#{ref_link}#{suffix}" - else - nil - end + text.html_safe end - # Return true if the +prefix+ and +suffix+ indicate that the matched string - # is an HTML entity like & - def html_entity?(prefix, suffix) - prefix && suffix && prefix[0] == '&' && suffix[-1] == ';' - end + private - # Private: Dispatches to a dedicated processing method based on reference + # Filters used in our pipeline # - # reference - Object reference ("@1234", "!567", etc.) - # identifier - Object identifier (Issue ID, SHA hash, etc.) + # SanitizationFilter should come first so that all generated reference HTML + # goes through untouched. # - # Returns string rendered by the processing method - def reference_link(type, identifier, project = @project, prefix_text = nil) - send("reference_#{type}", identifier, project, prefix_text) - end - - def reference_user(identifier, project = @project, _ = nil) - options = html_options.merge( - class: "gfm gfm-team_member #{html_options[:class]}" - ) - - if identifier == "all" - link_to("@all", project_url(project), options) - elsif User.find_by(username: identifier) - link_to("@#{identifier}", user_url(identifier), options) - end - end - - def reference_issue(identifier, project = @project, prefix_text = nil) - if project.used_default_issues_tracker? || !external_issues_tracker_enabled? - if project.issue_exists? identifier - url = url_for_issue(identifier, project) - title = title_for_issue(identifier, project) - options = html_options.merge( - title: "Issue: #{title}", - class: "gfm gfm-issue #{html_options[:class]}" - ) - - link_to("#{prefix_text}##{identifier}", url, options) - end - else - config = Gitlab.config - external_issue_tracker = config.issues_tracker[project.issues_tracker] - if external_issue_tracker.present? - reference_external_issue(identifier, external_issue_tracker, project, - prefix_text) - end - end - end - - def reference_merge_request(identifier, project = @project, - prefix_text = nil) - if merge_request = project.merge_requests.find_by(iid: identifier) - options = html_options.merge( - title: "Merge Request: #{merge_request.title}", - class: "gfm gfm-merge_request #{html_options[:class]}" - ) - url = project_merge_request_url(project, merge_request) - link_to("#{prefix_text}!#{identifier}", url, options) - end - end - - def reference_snippet(identifier, project = @project, _ = nil) - if snippet = project.snippets.find_by(id: identifier) - options = html_options.merge( - title: "Snippet: #{snippet.title}", - class: "gfm gfm-snippet #{html_options[:class]}" - ) - link_to("$#{identifier}", project_snippet_url(project, snippet), - options) - end - end - - def reference_commit(identifier, project = @project, prefix_text = nil) - if project.valid_repo? && commit = project.repository.commit(identifier) - options = html_options.merge( - title: commit.link_title, - class: "gfm gfm-commit #{html_options[:class]}" - ) - prefix_text = "#{prefix_text}@" if prefix_text - link_to( - "#{prefix_text}#{identifier}", - project_commit_url(project, commit), - options - ) - end - end - - def reference_external_issue(identifier, issue_tracker, project = @project, - prefix_text = nil) - url = url_for_issue(identifier, project) - title = issue_tracker['title'] - - options = html_options.merge( - title: "Issue in #{title}", - class: "gfm gfm-issue #{html_options[:class]}" - ) - link_to("#{prefix_text}##{identifier}", url, options) - end - - # Turn list items that start with "[ ]" into HTML checkbox inputs. - def parse_tasks(text) - li_tag = '<li class="task-list-item">' - unchecked_box = '<input type="checkbox" value="on" disabled />' - checked_box = unchecked_box.sub(/\/>$/, 'checked="checked" />') - - # Regexp captures don't seem to work when +text+ is an - # ActiveSupport::SafeBuffer, hence the `String.new` - String.new(text).gsub(Taskable::TASK_PATTERN_HTML) do - checked = $LAST_MATCH_INFO[:checked].downcase == 'x' - - if checked - "#{li_tag}#{checked_box}" - else - "#{li_tag}#{unchecked_box}" - end - end + # See https://github.com/jch/html-pipeline#filters for more filters. + def filters + [ + Gitlab::Markdown::SanitizationFilter, + + Gitlab::Markdown::RelativeLinkFilter, + Gitlab::Markdown::EmojiFilter, + Gitlab::Markdown::TableOfContentsFilter, + Gitlab::Markdown::AutolinkFilter, + Gitlab::Markdown::ExternalLinkFilter, + + Gitlab::Markdown::UserReferenceFilter, + Gitlab::Markdown::IssueReferenceFilter, + Gitlab::Markdown::ExternalIssueReferenceFilter, + Gitlab::Markdown::MergeRequestReferenceFilter, + Gitlab::Markdown::SnippetReferenceFilter, + Gitlab::Markdown::CommitRangeReferenceFilter, + Gitlab::Markdown::CommitReferenceFilter, + Gitlab::Markdown::LabelReferenceFilter, + + Gitlab::Markdown::TaskListFilter + ] end end end diff --git a/lib/gitlab/markdown/autolink_filter.rb b/lib/gitlab/markdown/autolink_filter.rb new file mode 100644 index 00000000000..4e14a048cfb --- /dev/null +++ b/lib/gitlab/markdown/autolink_filter.rb @@ -0,0 +1,100 @@ +require 'html/pipeline/filter' +require 'uri' + +module Gitlab + module Markdown + # HTML Filter for auto-linking URLs in HTML. + # + # Based on HTML::Pipeline::AutolinkFilter + # + # Context options: + # :autolink - Boolean, skips all processing done by this filter when false + # :link_attr - Hash of attributes for the generated links + # + class AutolinkFilter < HTML::Pipeline::Filter + include ActionView::Helpers::TagHelper + + # Pattern to match text that should be autolinked. + # + # A URI scheme begins with a letter and may contain letters, numbers, + # plus, period and hyphen. Schemes are case-insensitive but we're being + # picky here and allowing only lowercase for autolinks. + # + # See http://en.wikipedia.org/wiki/URI_scheme + # + # The negative lookbehind ensures that users can paste a URL followed by a + # period or comma for punctuation without those characters being included + # in the generated link. + # + # Rubular: http://rubular.com/r/cxjPyZc7Sb + LINK_PATTERN = %r{([a-z][a-z0-9\+\.-]+://\S+)(?<!,|\.)} + + # Text matching LINK_PATTERN inside these elements will not be linked + IGNORE_PARENTS = %w(a code kbd pre script style).to_set + + def call + return doc if context[:autolink] == false + + rinku_parse + text_parse + end + + private + + # Run the text through Rinku as a first pass + # + # This will quickly autolink http(s) and ftp links. + # + # `@doc` will be re-parsed with the HTML String from Rinku. + def rinku_parse + # Convert the options from a Hash to a String that Rinku expects + options = tag_options(link_options) + + # NOTE: We don't parse email links because it will erroneously match + # external Commit and CommitRange references. + # + # The final argument tells Rinku to link short URLs that don't include a + # period (e.g., http://localhost:3000/) + rinku = Rinku.auto_link(html, :urls, options, IGNORE_PARENTS.to_a, 1) + + # Rinku returns a String, so parse it back to a Nokogiri::XML::Document + # for further processing. + @doc = parse_html(rinku) + end + + # Autolinks any text matching LINK_PATTERN that Rinku didn't already + # replace + def text_parse + search_text_nodes(doc).each do |node| + content = node.to_html + + next if has_ancestor?(node, IGNORE_PARENTS) + next unless content.match(LINK_PATTERN) + + # If Rinku didn't link this, there's probably a good reason, so we'll + # skip it too + next if content.start_with?(*%w(http https ftp)) + + html = autolink_filter(content) + + next if html == content + + node.replace(html) + end + + doc + end + + def autolink_filter(text) + text.gsub(LINK_PATTERN) do |match| + options = link_options.merge(href: match) + content_tag(:a, match, options) + end + end + + def link_options + @link_options ||= context[:link_attr] || {} + end + end + end +end diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb new file mode 100644 index 00000000000..61591a9914b --- /dev/null +++ b/lib/gitlab/markdown/commit_range_reference_filter.rb @@ -0,0 +1,79 @@ +module Gitlab + module Markdown + # HTML filter that replaces commit range references with links. + # + # This filter supports cross-project references. + class CommitRangeReferenceFilter < ReferenceFilter + include CrossProjectReference + + # Public: Find commit range references in text + # + # CommitRangeReferenceFilter.references_in(text) do |match, commit_range, project_ref| + # "<a href=...>#{commit_range}</a>" + # end + # + # text - String text to search. + # + # Yields the String match, the String commit range, and an optional String + # of the external project reference. + # + # Returns a String replaced with the return of the block. + def self.references_in(text) + text.gsub(CommitRange.reference_pattern) do |match| + yield match, $~[:commit_range], $~[:project] + end + end + + def initialize(*args) + super + + @commit_map = {} + end + + def call + replace_text_nodes_matching(CommitRange.reference_pattern) do |content| + commit_range_link_filter(content) + end + end + + # Replace commit range references in text with links to compare the commit + # ranges. + # + # text - String text to replace references in. + # + # Returns a String with commit range references replaced with links. All + # links have `gfm` and `gfm-commit_range` class names attached for + # styling. + def commit_range_link_filter(text) + self.class.references_in(text) do |match, id, project_ref| + project = self.project_from_ref(project_ref) + + range = CommitRange.new(id, project) + + if range.valid_commits? + push_result(:commit_range, range) + + url = url_for_commit_range(project, range) + + title = range.reference_title + klass = reference_class(:commit_range) + + project_ref += '@' if project_ref + + %(<a href="#{url}" + title="#{title}" + class="#{klass}">#{project_ref}#{range}</a>) + else + match + end + end + end + + def url_for_commit_range(project, range) + h = Rails.application.routes.url_helpers + h.namespace_project_compare_url(project.namespace, project, + range.to_param.merge(only_path: context[:only_path])) + end + end + end +end diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb new file mode 100644 index 00000000000..f6932e76e70 --- /dev/null +++ b/lib/gitlab/markdown/commit_reference_filter.rb @@ -0,0 +1,75 @@ +module Gitlab + module Markdown + # HTML filter that replaces commit references with links. + # + # This filter supports cross-project references. + class CommitReferenceFilter < ReferenceFilter + include CrossProjectReference + + # Public: Find commit references in text + # + # CommitReferenceFilter.references_in(text) do |match, commit, project_ref| + # "<a href=...>#{commit}</a>" + # end + # + # text - String text to search. + # + # Yields the String match, the String commit identifier, and an optional + # String of the external project reference. + # + # Returns a String replaced with the return of the block. + def self.references_in(text) + text.gsub(Commit.reference_pattern) do |match| + yield match, $~[:commit], $~[:project] + end + end + + def call + replace_text_nodes_matching(Commit.reference_pattern) do |content| + commit_link_filter(content) + end + end + + # Replace commit references in text with links to the commit specified. + # + # text - String text to replace references in. + # + # Returns a String with commit references replaced with links. All links + # have `gfm` and `gfm-commit` class names attached for styling. + def commit_link_filter(text) + self.class.references_in(text) do |match, commit_ref, project_ref| + project = self.project_from_ref(project_ref) + + if commit = commit_from_ref(project, commit_ref) + push_result(:commit, commit) + + url = url_for_commit(project, commit) + + title = escape_once(commit.link_title) + klass = reference_class(:commit) + + project_ref += '@' if project_ref + + %(<a href="#{url}" + title="#{title}" + class="#{klass}">#{project_ref}#{commit.short_id}</a>) + else + match + end + end + end + + def commit_from_ref(project, commit_ref) + if project && project.valid_repo? + project.commit(commit_ref) + end + end + + def url_for_commit(project, commit) + h = Rails.application.routes.url_helpers + h.namespace_project_commit_url(project.namespace, project, commit, + only_path: context[:only_path]) + end + end + end +end diff --git a/lib/gitlab/markdown/cross_project_reference.rb b/lib/gitlab/markdown/cross_project_reference.rb new file mode 100644 index 00000000000..66c256c5104 --- /dev/null +++ b/lib/gitlab/markdown/cross_project_reference.rb @@ -0,0 +1,29 @@ +module Gitlab + module Markdown + # Common methods for ReferenceFilters that support an optional cross-project + # reference. + module CrossProjectReference + # Given a cross-project reference string, get the Project record + # + # Defaults to value of `context[:project]` if: + # * No reference is given OR + # * Reference given doesn't exist + # + # ref - String reference. + # + # Returns a Project, or nil if the reference can't be accessed + def project_from_ref(ref) + return context[:project] unless ref + + other = Project.find_with_namespace(ref) + return nil unless other && user_can_reference_project?(other) + + other + end + + def user_can_reference_project?(project, user = context[:current_user]) + Ability.abilities.allowed?(user, :read_project, project) + end + end + end +end diff --git a/lib/gitlab/markdown/emoji_filter.rb b/lib/gitlab/markdown/emoji_filter.rb new file mode 100644 index 00000000000..6794ab9c897 --- /dev/null +++ b/lib/gitlab/markdown/emoji_filter.rb @@ -0,0 +1,79 @@ +require 'gitlab_emoji' +require 'html/pipeline/filter' +require 'action_controller' + +module Gitlab + module Markdown + # HTML filter that replaces :emoji: with images. + # + # Based on HTML::Pipeline::EmojiFilter + # + # Context options: + # :asset_root + # :asset_host + class EmojiFilter < HTML::Pipeline::Filter + IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set + + def call + search_text_nodes(doc).each do |node| + content = node.to_html + next unless content.include?(':') + next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) + + html = emoji_image_filter(content) + + next if html == content + + node.replace(html) + end + + doc + end + + # Replace :emoji: with corresponding images. + # + # text - String text to replace :emoji: in. + # + # Returns a String with :emoji: replaced with images. + def emoji_image_filter(text) + text.gsub(emoji_pattern) do |match| + name = $1 + "<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{emoji_url(name)}' height='20' width='20' align='absmiddle' />" + end + end + + private + + def emoji_url(name) + emoji_path = "emoji/#{emoji_filename(name)}" + if context[:asset_host] + # Asset host is specified. + url_to_image(emoji_path) + elsif context[:asset_root] + # Gitlab url is specified + File.join(context[:asset_root], url_to_image(emoji_path)) + else + # All other cases + url_to_image(emoji_path) + end + end + + def url_to_image(image) + ActionController::Base.helpers.url_to_image(image) + end + + # Build a regexp that matches all valid :emoji: names. + def self.emoji_pattern + @emoji_pattern ||= /:(#{Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/ + end + + def emoji_pattern + self.class.emoji_pattern + end + + def emoji_filename(name) + "#{Emoji.emoji_filename(name)}.png" + end + end + end +end diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/external_issue_reference_filter.rb new file mode 100644 index 00000000000..afd28dd8cf3 --- /dev/null +++ b/lib/gitlab/markdown/external_issue_reference_filter.rb @@ -0,0 +1,60 @@ +module Gitlab + module Markdown + # HTML filter that replaces external issue tracker references with links. + # References are ignored if the project doesn't use an external issue + # tracker. + class ExternalIssueReferenceFilter < ReferenceFilter + # Public: Find `JIRA-123` issue references in text + # + # ExternalIssueReferenceFilter.references_in(text) do |match, issue| + # "<a href=...>##{issue}</a>" + # end + # + # text - String text to search. + # + # Yields the String match and the String issue reference. + # + # Returns a String replaced with the return of the block. + def self.references_in(text) + text.gsub(ExternalIssue.reference_pattern) do |match| + yield match, $~[:issue] + end + end + + def call + # Early return if the project isn't using an external tracker + return doc if project.nil? || project.default_issues_tracker? + + replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content| + issue_link_filter(content) + end + end + + # Replace `JIRA-123` issue references in text with links to the referenced + # issue's details page. + # + # text - String text to replace references in. + # + # Returns a String with `JIRA-123` references replaced with links. All + # links have `gfm` and `gfm-issue` class names attached for styling. + def issue_link_filter(text) + project = context[:project] + + self.class.references_in(text) do |match, issue| + url = url_for_issue(issue, project, only_path: context[:only_path]) + + title = escape_once("Issue in #{project.external_issue_tracker.title}") + klass = reference_class(:issue) + + %(<a href="#{url}" + title="#{title}" + class="#{klass}">#{match}</a>) + end + end + + def url_for_issue(*args) + IssuesHelper.url_for_issue(*args) + end + end + end +end diff --git a/lib/gitlab/markdown/external_link_filter.rb b/lib/gitlab/markdown/external_link_filter.rb new file mode 100644 index 00000000000..c539e0fb823 --- /dev/null +++ b/lib/gitlab/markdown/external_link_filter.rb @@ -0,0 +1,33 @@ +require 'html/pipeline/filter' + +module Gitlab + module Markdown + # HTML Filter to add a `rel="nofollow"` attribute to external links + # + class ExternalLinkFilter < HTML::Pipeline::Filter + def call + doc.search('a').each do |node| + next unless node.has_attribute?('href') + + link = node.attribute('href').value + + # Skip non-HTTP(S) links + next unless link.start_with?('http') + + # Skip internal links + next if link.start_with?(internal_url) + + node.set_attribute('rel', 'nofollow') + end + + doc + end + + private + + def internal_url + @internal_url ||= Gitlab.config.gitlab.url + end + end + end +end diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb new file mode 100644 index 00000000000..dea04761ead --- /dev/null +++ b/lib/gitlab/markdown/issue_reference_filter.rb @@ -0,0 +1,67 @@ +module Gitlab + module Markdown + # HTML filter that replaces issue references with links. References to + # issues that do not exist are ignored. + # + # This filter supports cross-project references. + class IssueReferenceFilter < ReferenceFilter + include CrossProjectReference + + # Public: Find `#123` issue references in text + # + # IssueReferenceFilter.references_in(text) do |match, issue, project_ref| + # "<a href=...>##{issue}</a>" + # end + # + # text - String text to search. + # + # Yields the String match, the Integer issue ID, and an optional String of + # the external project reference. + # + # Returns a String replaced with the return of the block. + def self.references_in(text) + text.gsub(Issue.reference_pattern) do |match| + yield match, $~[:issue].to_i, $~[:project] + end + end + + def call + replace_text_nodes_matching(Issue.reference_pattern) do |content| + issue_link_filter(content) + end + end + + # Replace `#123` issue references in text with links to the referenced + # issue's details page. + # + # text - String text to replace references in. + # + # Returns a String with `#123` references replaced with links. All links + # have `gfm` and `gfm-issue` class names attached for styling. + def issue_link_filter(text) + self.class.references_in(text) do |match, id, project_ref| + project = self.project_from_ref(project_ref) + + if project && issue = project.get_issue(id) + push_result(:issue, issue) + + url = url_for_issue(id, project, only_path: context[:only_path]) + + title = escape_once("Issue: #{issue.title}") + klass = reference_class(:issue) + + %(<a href="#{url}" + title="#{title}" + class="#{klass}">#{match}</a>) + else + match + end + end + end + + def url_for_issue(*args) + IssuesHelper.url_for_issue(*args) + end + end + end +end diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb new file mode 100644 index 00000000000..e022ca69c91 --- /dev/null +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -0,0 +1,82 @@ +module Gitlab + module Markdown + # HTML filter that replaces label references with links. + class LabelReferenceFilter < ReferenceFilter + # Public: Find label references in text + # + # LabelReferenceFilter.references_in(text) do |match, id, name| + # "<a href=...>#{Label.find(id)}</a>" + # end + # + # text - String text to search. + # + # Yields the String match, an optional Integer label ID, and an optional + # String label name. + # + # Returns a String replaced with the return of the block. + def self.references_in(text) + text.gsub(Label.reference_pattern) do |match| + yield match, $~[:label_id].to_i, $~[:label_name] + end + end + + def call + replace_text_nodes_matching(Label.reference_pattern) do |content| + label_link_filter(content) + end + end + + # Replace label references in text with links to the label specified. + # + # text - String text to replace references in. + # + # Returns a String with label references replaced with links. All links + # have `gfm` and `gfm-label` class names attached for styling. + def label_link_filter(text) + project = context[:project] + + self.class.references_in(text) do |match, id, name| + params = label_params(id, name) + + if label = project.labels.find_by(params) + push_result(:label, label) + + url = url_for_label(project, label) + klass = reference_class(:label) + + %(<a href="#{url}" + class="#{klass}">#{render_colored_label(label)}</a>) + else + match + end + end + end + + def url_for_label(project, label) + h = Rails.application.routes.url_helpers + h.namespace_project_issues_path(project.namespace, project, + label_name: label.name, + only_path: context[:only_path]) + end + + def render_colored_label(label) + LabelsHelper.render_colored_label(label) + end + + # Parameters to pass to `Label.find_by` based on the given arguments + # + # id - Integer ID to pass. If present, returns {id: id} + # name - String name to pass. If `id` is absent, finds by name without + # surrounding quotes. + # + # Returns a Hash. + def label_params(id, name) + if name + { name: name.tr('"', '') } + else + { id: id } + end + end + end + end +end diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb new file mode 100644 index 00000000000..80779819485 --- /dev/null +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -0,0 +1,69 @@ +module Gitlab + module Markdown + # HTML filter that replaces merge request references with links. References + # to merge requests that do not exist are ignored. + # + # This filter supports cross-project references. + class MergeRequestReferenceFilter < ReferenceFilter + include CrossProjectReference + + # Public: Find `!123` merge request references in text + # + # MergeRequestReferenceFilter.references_in(text) do |match, merge_request, project_ref| + # "<a href=...>##{merge_request}</a>" + # end + # + # text - String text to search. + # + # Yields the String match, the Integer merge request ID, and an optional + # String of the external project reference. + # + # Returns a String replaced with the return of the block. + def self.references_in(text) + text.gsub(MergeRequest.reference_pattern) do |match| + yield match, $~[:merge_request].to_i, $~[:project] + end + end + + def call + replace_text_nodes_matching(MergeRequest.reference_pattern) do |content| + merge_request_link_filter(content) + end + end + + # Replace `!123` merge request references in text with links to the + # referenced merge request's details page. + # + # text - String text to replace references in. + # + # Returns a String with `!123` references replaced with links. All links + # have `gfm` and `gfm-merge_request` class names attached for styling. + def merge_request_link_filter(text) + self.class.references_in(text) do |match, id, project_ref| + project = self.project_from_ref(project_ref) + + if project && merge_request = project.merge_requests.find_by(iid: id) + push_result(:merge_request, merge_request) + + title = escape_once("Merge Request: #{merge_request.title}") + klass = reference_class(:merge_request) + + url = url_for_merge_request(merge_request, project) + + %(<a href="#{url}" + title="#{title}" + class="#{klass}">#{match}</a>) + else + match + end + end + end + + def url_for_merge_request(mr, project) + h = Rails.application.routes.url_helpers + h.namespace_project_merge_request_url(project.namespace, project, mr, + only_path: context[:only_path]) + end + end + end +end diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb new file mode 100644 index 00000000000..a84bacd3d4f --- /dev/null +++ b/lib/gitlab/markdown/reference_filter.rb @@ -0,0 +1,100 @@ +require 'active_support/core_ext/string/output_safety' +require 'html/pipeline/filter' + +module Gitlab + module Markdown + # Base class for GitLab Flavored Markdown reference filters. + # + # References within <pre>, <code>, <a>, and <style> elements are ignored. + # + # Context options: + # :project (required) - Current project, ignored if reference is cross-project. + # :reference_class - Custom CSS class added to reference links. + # :only_path - Generate path-only links. + # + # Results: + # :references - A Hash of references that were found and replaced. + class ReferenceFilter < HTML::Pipeline::Filter + def initialize(*args) + super + + result[:references] = Hash.new { |hash, type| hash[type] = [] } + end + + def escape_once(html) + ERB::Util.html_escape_once(html) + end + + def ignore_parents + @ignore_parents ||= begin + # Don't look for references in text nodes that are children of these + # elements. + parents = %w(pre code a style) + parents << 'blockquote' if context[:ignore_blockquotes] + parents.to_set + end + end + + def ignored_ancestry?(node) + has_ancestor?(node, ignore_parents) + end + + def project + context[:project] + end + + # Add a reference to the pipeline's result Hash + # + # type - Singular Symbol reference type (e.g., :issue, :user, etc.) + # values - One or more Objects to add + def push_result(type, *values) + return if values.empty? + + result[:references][type].push(*values) + end + + def reference_class(type) + "gfm gfm-#{type} #{context[:reference_class]}".strip + end + + # Iterate through the document's text nodes, yielding the current node's + # content if: + # + # * The `project` context value is present AND + # * The node's content matches `pattern` AND + # * The node is not an ancestor of an ignored node type + # + # pattern - Regex pattern against which to match the node's content + # + # Yields the current node's String contents. The result of the block will + # replace the node's existing content and update the current document. + # + # Returns the updated Nokogiri::XML::Document object. + def replace_text_nodes_matching(pattern) + return doc if project.nil? + + search_text_nodes(doc).each do |node| + content = node.to_html + + next unless content.match(pattern) + next if ignored_ancestry?(node) + + html = yield content + + next if html == content + + node.replace(html) + end + + doc + end + + # Ensure that a :project key exists in context + # + # Note that while the key might exist, its value could be nil! + def validate + needs :project + end + end + end +end diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/relative_link_filter.rb new file mode 100644 index 00000000000..9de2b24a9da --- /dev/null +++ b/lib/gitlab/markdown/relative_link_filter.rb @@ -0,0 +1,128 @@ +require 'html/pipeline/filter' +require 'uri' + +module Gitlab + module Markdown + # HTML filter that "fixes" relative links to files in a repository. + # + # Context options: + # :commit + # :project + # :project_wiki + # :ref + # :requested_path + class RelativeLinkFilter < HTML::Pipeline::Filter + def call + return doc unless linkable_files? + + doc.search('a').each do |el| + process_link_attr el.attribute('href') + end + + doc.search('img').each do |el| + process_link_attr el.attribute('src') + end + + doc + end + + protected + + def linkable_files? + context[:project_wiki].nil? && repository.try(:exists?) && !repository.empty? + end + + def process_link_attr(html_attr) + return if html_attr.blank? + + uri = URI(html_attr.value) + if uri.relative? && uri.path.present? + html_attr.value = rebuild_relative_uri(uri).to_s + end + rescue URI::Error + # noop + end + + def rebuild_relative_uri(uri) + file_path = relative_file_path(uri.path) + + uri.path = [ + relative_url_root, + context[:project].path_with_namespace, + path_type(file_path), + ref || 'master', # assume that if no ref exists we can point to master + file_path + ].compact.join('/').squeeze('/').chomp('/') + + uri + end + + def relative_file_path(path) + nested_path = build_nested_path(path, context[:requested_path]) + file_exists?(nested_path) ? nested_path : path + end + + # Covering a special case, when the link is referencing file in the same + # directory. + # If we are at doc/api/README.md and the README.md contains relative + # links like [Users](users.md), this takes the request + # path(doc/api/README.md) and replaces the README.md with users.md so the + # path looks like doc/api/users.md. + # If we are at doc/api and the README.md shown in below the tree view + # this takes the request path(doc/api) and adds users.md so the path + # looks like doc/api/users.md + def build_nested_path(path, request_path) + return request_path if path.empty? + return path unless request_path + + parts = request_path.split('/') + parts.pop if path_type(request_path) != 'tree' + parts.push(path).join('/') + end + + def file_exists?(path) + return false if path.nil? + repository.blob_at(current_sha, path).present? || + repository.tree(current_sha, path).entries.any? + end + + # Get the type of the given path + # + # path - String path to check + # + # Examples: + # + # path_type('doc/README.md') # => 'blob' + # path_type('doc/logo.png') # => 'raw' + # path_type('doc/api') # => 'tree' + # + # Returns a String + def path_type(path) + if repository.tree(current_sha, path).entries.any? + 'tree' + elsif repository.blob_at(current_sha, path).try(:image?) + 'raw' + else + 'blob' + end + end + + def current_sha + context[:commit].try(:id) || + ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha + end + + def relative_url_root + Gitlab.config.gitlab.relative_url_root.presence || '/' + end + + def ref + context[:ref] + end + + def repository + context[:project].try(:repository) + end + end + end +end diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/sanitization_filter.rb new file mode 100644 index 00000000000..74b3a8d274f --- /dev/null +++ b/lib/gitlab/markdown/sanitization_filter.rb @@ -0,0 +1,79 @@ +require 'html/pipeline/filter' +require 'html/pipeline/sanitization_filter' + +module Gitlab + module Markdown + # Sanitize HTML + # + # Extends HTML::Pipeline::SanitizationFilter with a custom whitelist. + class SanitizationFilter < HTML::Pipeline::SanitizationFilter + def whitelist + # Descriptions are more heavily sanitized, allowing only a few elements. + # See http://git.io/vkuAN + if pipeline == :description + whitelist = LIMITED + whitelist[:elements] -= %w(pre code img ol ul li) + else + whitelist = super + end + + customize_whitelist(whitelist) + + whitelist + end + + private + + def pipeline + context[:pipeline] || :default + end + + def customized?(transformers) + transformers.last.source_location[0] == __FILE__ + end + + def customize_whitelist(whitelist) + # Only push these customizations once + return if customized?(whitelist[:transformers]) + + # Allow code highlighting + whitelist[:attributes]['pre'] = %w(class) + whitelist[:attributes]['span'] = %w(class) + + # Allow table alignment + whitelist[:attributes]['th'] = %w(style) + whitelist[:attributes]['td'] = %w(style) + + # Allow span elements + whitelist[:elements].push('span') + + # Remove `rel` attribute from `a` elements + whitelist[:transformers].push(remove_rel) + + # Remove `class` attribute from non-highlight spans + whitelist[:transformers].push(clean_spans) + + whitelist + end + + def remove_rel + lambda do |env| + if env[:node_name] == 'a' + env[:node].remove_attribute('rel') + end + end + end + + def clean_spans + lambda do |env| + return unless env[:node_name] == 'span' + return unless env[:node].has_attribute?('class') + + unless has_ancestor?(env[:node], 'pre') + env[:node].remove_attribute('class') + end + end + end + end + end +end diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb new file mode 100644 index 00000000000..174ba58af6c --- /dev/null +++ b/lib/gitlab/markdown/snippet_reference_filter.rb @@ -0,0 +1,69 @@ +module Gitlab + module Markdown + # HTML filter that replaces snippet references with links. References to + # snippets that do not exist are ignored. + # + # This filter supports cross-project references. + class SnippetReferenceFilter < ReferenceFilter + include CrossProjectReference + + # Public: Find `$123` snippet references in text + # + # SnippetReferenceFilter.references_in(text) do |match, snippet| + # "<a href=...>$#{snippet}</a>" + # end + # + # text - String text to search. + # + # Yields the String match, the Integer snippet ID, and an optional String + # of the external project reference. + # + # Returns a String replaced with the return of the block. + def self.references_in(text) + text.gsub(Snippet.reference_pattern) do |match| + yield match, $~[:snippet].to_i, $~[:project] + end + end + + def call + replace_text_nodes_matching(Snippet.reference_pattern) do |content| + snippet_link_filter(content) + end + end + + # Replace `$123` snippet references in text with links to the referenced + # snippets's details page. + # + # text - String text to replace references in. + # + # Returns a String with `$123` references replaced with links. All links + # have `gfm` and `gfm-snippet` class names attached for styling. + def snippet_link_filter(text) + self.class.references_in(text) do |match, id, project_ref| + project = self.project_from_ref(project_ref) + + if project && snippet = project.snippets.find_by(id: id) + push_result(:snippet, snippet) + + title = escape_once("Snippet: #{snippet.title}") + klass = reference_class(:snippet) + + url = url_for_snippet(snippet, project) + + %(<a href="#{url}" + title="#{title}" + class="#{klass}">#{match}</a>) + else + match + end + end + end + + def url_for_snippet(snippet, project) + h = Rails.application.routes.url_helpers + h.namespace_project_snippet_url(project.namespace, project, snippet, + only_path: context[:only_path]) + end + end + end +end diff --git a/lib/gitlab/markdown/table_of_contents_filter.rb b/lib/gitlab/markdown/table_of_contents_filter.rb new file mode 100644 index 00000000000..38887c9778c --- /dev/null +++ b/lib/gitlab/markdown/table_of_contents_filter.rb @@ -0,0 +1,62 @@ +require 'html/pipeline/filter' + +module Gitlab + module Markdown + # HTML filter that adds an anchor child element to all Headers in a + # document, so that they can be linked to. + # + # Generates the Table of Contents with links to each header. See Results. + # + # Based on HTML::Pipeline::TableOfContentsFilter. + # + # Context options: + # :no_header_anchors - Skips all processing done by this filter. + # + # Results: + # :toc - String containing Table of Contents data as a `ul` element with + # `li` child elements. + class TableOfContentsFilter < HTML::Pipeline::Filter + PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u + + def call + return doc if context[:no_header_anchors] + + result[:toc] = "" + + headers = Hash.new(0) + + doc.css('h1, h2, h3, h4, h5, h6').each do |node| + text = node.text + + id = text.downcase + id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation + id.gsub!(' ', '-') # replace spaces with dash + id.squeeze!('-') # replace multiple dashes with one + + uniq = (headers[id] > 0) ? "-#{headers[id]}" : '' + headers[id] += 1 + + if header_content = node.children.first + href = "#{id}#{uniq}" + push_toc(href, text) + header_content.add_previous_sibling(anchor_tag(href)) + end + end + + result[:toc] = %Q{<ul class="section-nav">\n#{result[:toc]}</ul>} unless result[:toc].empty? + + doc + end + + private + + def anchor_tag(href) + %Q{<a id="#{href}" class="anchor" href="##{href}" aria-hidden="true"></a>} + end + + def push_toc(href, text) + result[:toc] << %Q{<li><a href="##{href}">#{text}</a></li>\n} + end + end + end +end diff --git a/lib/gitlab/markdown/task_list_filter.rb b/lib/gitlab/markdown/task_list_filter.rb new file mode 100644 index 00000000000..c6eb2e2bf6d --- /dev/null +++ b/lib/gitlab/markdown/task_list_filter.rb @@ -0,0 +1,23 @@ +require 'task_list/filter' + +module Gitlab + module Markdown + # Work around a bug in the default TaskList::Filter that adds a `task-list` + # class to every list element, regardless of whether or not it contains a + # task list. + # + # This is a (hopefully) temporary fix, pending a new release of the + # task_list gem. + # + # See https://github.com/github/task_list/pull/60 + class TaskListFilter < TaskList::Filter + def add_css_class(node, *new_class_names) + if new_class_names.include?('task-list') + super if node.children.any? { |c| c['class'] == 'task-list-item' } + else + super + end + end + end + end +end diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb new file mode 100644 index 00000000000..c9972957182 --- /dev/null +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -0,0 +1,105 @@ +module Gitlab + module Markdown + # HTML filter that replaces user or group references with links. + # + # A special `@all` reference is also supported. + class UserReferenceFilter < ReferenceFilter + # Public: Find `@user` user references in text + # + # UserReferenceFilter.references_in(text) do |match, username| + # "<a href=...>@#{user}</a>" + # end + # + # text - String text to search. + # + # Yields the String match, and the String user name. + # + # Returns a String replaced with the return of the block. + def self.references_in(text) + text.gsub(User.reference_pattern) do |match| + yield match, $~[:user] + end + end + + def call + replace_text_nodes_matching(User.reference_pattern) do |content| + user_link_filter(content) + end + end + + # Replace `@user` user references in text with links to the referenced + # user's profile page. + # + # text - String text to replace references in. + # + # Returns a String with `@user` references replaced with links. All links + # have `gfm` and `gfm-project_member` class names attached for styling. + def user_link_filter(text) + self.class.references_in(text) do |match, username| + if username == 'all' + link_to_all + elsif namespace = Namespace.find_by(path: username) + link_to_namespace(namespace) || match + else + match + end + end + end + + private + + def urls + Rails.application.routes.url_helpers + end + + def link_class + reference_class(:project_member) + end + + def link_to_all + project = context[:project] + + # FIXME (rspeicher): Law of Demeter + push_result(:user, *project.team.members.flatten) + + url = urls.namespace_project_url(project.namespace, project, + only_path: context[:only_path]) + + text = User.reference_prefix + 'all' + %(<a href="#{url}" class="#{link_class}">#{text}</a>) + end + + def link_to_namespace(namespace) + if namespace.is_a?(Group) + link_to_group(namespace.path, namespace) + else + link_to_user(namespace.path, namespace) + end + end + + def link_to_group(group, namespace) + return unless user_can_reference_group?(namespace) + + push_result(:user, *namespace.users) + + url = urls.group_url(group, only_path: context[:only_path]) + + text = Group.reference_prefix + group + %(<a href="#{url}" class="#{link_class}">#{text}</a>) + end + + def link_to_user(user, namespace) + push_result(:user, namespace.owner) + + url = urls.user_url(user, only_path: context[:only_path]) + + text = User.reference_prefix + user + %(<a href="#{url}" class="#{link_class}">#{text}</a>) + end + + def user_can_reference_group?(group) + Ability.abilities.allowed?(context[:current_user], :read_group, group) + end + end + end +end diff --git a/lib/gitlab/markdown_helper.rb b/lib/gitlab/markup_helper.rb index 5e3cfc0585b..f99be969d3e 100644 --- a/lib/gitlab/markdown_helper.rb +++ b/lib/gitlab/markup_helper.rb @@ -1,5 +1,5 @@ module Gitlab - module MarkdownHelper + module MarkupHelper module_function # Public: Determines if a given filename is compatible with GitHub::Markup. @@ -8,8 +8,10 @@ module Gitlab # # Returns boolean def markup?(filename) - filename.downcase.end_with?(*%w(.textile .rdoc .org .creole .wiki - .mediawiki .rst .adoc .asciidoc .asc)) + gitlab_markdown?(filename) || + asciidoc?(filename) || + filename.downcase.end_with?(*%w(.textile .rdoc .org .creole .wiki + .mediawiki .rst)) end # Public: Determines if a given filename is compatible with @@ -22,8 +24,17 @@ module Gitlab filename.downcase.end_with?(*%w(.mdown .md .markdown)) end + # Public: Determines if the given filename has AsciiDoc extension. + # + # filename - Filename string to check + # + # Returns boolean + def asciidoc?(filename) + filename.downcase.end_with?(*%w(.adoc .ad .asciidoc)) + end + def previewable?(filename) - gitlab_markdown?(filename) || markup?(filename) + markup?(filename) end end end diff --git a/lib/gitlab/middleware/static.rb b/lib/gitlab/middleware/static.rb new file mode 100644 index 00000000000..85ffa8aca68 --- /dev/null +++ b/lib/gitlab/middleware/static.rb @@ -0,0 +1,13 @@ +module Gitlab + module Middleware + class Static < ActionDispatch::Static + UPLOADS_REGEX = /\A\/uploads(\/|\z)/.freeze + + def call(env) + return @app.call(env) if env['PATH_INFO'] =~ UPLOADS_REGEX + + super + end + end + end +end diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb new file mode 100644 index 00000000000..ea6b0ee796d --- /dev/null +++ b/lib/gitlab/note_data_builder.rb @@ -0,0 +1,77 @@ +module Gitlab + class NoteDataBuilder + class << self + # Produce a hash of post-receive data + # + # For all notes: + # + # data = { + # object_kind: "note", + # user: { + # name: String, + # username: String, + # avatar_url: String + # } + # project_id: Integer, + # repository: { + # name: String, + # url: String, + # description: String, + # homepage: String, + # } + # object_attributes: { + # <hook data for note> + # } + # <note-specific data>: { + # } + # note-specific data is a hash with one of the following keys and contains + # the hook data for that type. + # - commit + # - issue + # - merge_request + # - snippet + # + def build(note, user) + project = note.project + data = build_base_data(project, user, note) + + if note.for_commit? + data[:commit] = build_data_for_commit(project, user, note) + elsif note.for_issue? + data[:issue] = note.noteable.hook_attrs + elsif note.for_merge_request? + data[:merge_request] = note.noteable.hook_attrs + elsif note.for_project_snippet? + data[:snippet] = note.noteable.hook_attrs + end + + data + end + + def build_base_data(project, user, note) + base_data = { + object_kind: "note", + user: user.hook_attrs, + project_id: project.id, + repository: { + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url, + }, + object_attributes: note.hook_attrs + } + + base_data[:object_attributes][:url] = + Gitlab::UrlBuilder.new(:note).build(note.id) + base_data + end + + def build_data_for_commit(project, user, note) + # commit_id is the SHA hash + commit = project.commit(note.commit_id) + commit.hook_attrs + end + end + end +end diff --git a/lib/gitlab/oauth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb index ce52beec78e..0f16c925900 100644 --- a/lib/gitlab/oauth/auth_hash.rb +++ b/lib/gitlab/o_auth/auth_hash.rb @@ -9,11 +9,11 @@ module Gitlab end def uid - auth_hash.uid.to_s + Gitlab::Utils.force_utf8(auth_hash.uid.to_s) end def provider - auth_hash.provider + Gitlab::Utils.force_utf8(auth_hash.provider.to_s) end def info @@ -21,23 +21,28 @@ module Gitlab end def name - (info.try(:name) || full_name).to_s.force_encoding('utf-8') + Gitlab::Utils.force_utf8((info.try(:name) || full_name).to_s) end def full_name - "#{info.first_name} #{info.last_name}" + Gitlab::Utils.force_utf8("#{info.first_name} #{info.last_name}") end def username - (info.try(:nickname) || generate_username).to_s.force_encoding('utf-8') + Gitlab::Utils.force_utf8( + (info.try(:nickname) || generate_username).to_s + ) end def email - (info.try(:email) || generate_temporarily_email).downcase + Gitlab::Utils.force_utf8( + (info.try(:email) || generate_temporarily_email).downcase + ) end def password - @password ||= Devise.friendly_token[0, 8].downcase + devise_friendly_token = Devise.friendly_token[0, 8].downcase + @password ||= Gitlab::Utils.force_utf8(devise_friendly_token) end # Get the first part of the email address (before @) diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb new file mode 100644 index 00000000000..17ce4d4b174 --- /dev/null +++ b/lib/gitlab/o_auth/user.rb @@ -0,0 +1,164 @@ +# OAuth extension for User model +# +# * Find GitLab user based on omniauth uid and provider +# * Create new user from omniauth data +# +module Gitlab + module OAuth + class SignupDisabledError < StandardError; end + + class User + attr_accessor :auth_hash, :gl_user + + def initialize(auth_hash) + self.auth_hash = auth_hash + end + + def persisted? + gl_user.try(:persisted?) + end + + def new? + !persisted? + end + + def valid? + gl_user.try(:valid?) + end + + def save + unauthorized_to_create unless gl_user + + if needs_blocking? + gl_user.save! + gl_user.block + else + gl_user.save! + end + + log.info "(OAuth) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" + gl_user + rescue ActiveRecord::RecordInvalid => e + log.info "(OAuth) Error saving user: #{gl_user.errors.full_messages}" + return self, e.record.errors + end + + def gl_user + @user ||= find_by_uid_and_provider + + if auto_link_ldap_user? + @user ||= find_or_create_ldap_user + end + + if signup_enabled? + @user ||= build_new_user + end + + @user + end + + protected + + def find_or_create_ldap_user + 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.downcase, ldap_person.provider) + # Case when a LDAP user already exists in Gitlab. Add the Omniauth identity to existing account. + 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 + user.identities.new(provider: ldap_person.provider, extern_uid: ldap_person.dn) + end + + user + end + + def auto_link_ldap_user? + Gitlab.config.omniauth.auto_link_ldap_user + end + + def creating_linked_ldap_user? + auto_link_ldap_user? && ldap_person + end + + def ldap_person + return @ldap_person if defined?(@ldap_person) + + # Look for a corresponding person with same uid in any of the configured LDAP providers + Gitlab::LDAP::Config.providers.each do |provider| + adapter = Gitlab::LDAP::Adapter.new(provider) + @ldap_person = Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter) + break if @ldap_person + end + @ldap_person + end + + def ldap_config + Gitlab::LDAP::Config.new(ldap_person.provider) if ldap_person + end + + def needs_blocking? + new? && block_after_signup? + end + + def signup_enabled? + Gitlab.config.omniauth.allow_single_sign_on + end + + def block_after_signup? + if creating_linked_ldap_user? + ldap_config.block_auto_created_users + else + Gitlab.config.omniauth.block_auto_created_users + end + end + + def auth_hash=(auth_hash) + @auth_hash = AuthHash.new(auth_hash) + end + + def find_by_uid_and_provider + identity = Identity.find_by(provider: auth_hash.provider, extern_uid: auth_hash.uid) + identity && identity.user + end + + def build_new_user + user = ::User.new(user_attributes) + user.skip_confirmation! + user.identities.new(extern_uid: auth_hash.uid, provider: auth_hash.provider) + user + end + + def user_attributes + # Give preference to LDAP for sensitive information when creating a linked account + if creating_linked_ldap_user? + username = ldap_person.username + email = ldap_person.email.first + else + username = auth_hash.username + email = auth_hash.email + end + + { + name: auth_hash.name, + username: ::Namespace.clean_path(username), + email: email, + password: auth_hash.password, + password_confirmation: auth_hash.password, + password_automatically_set: true + } + end + + def log + Gitlab::AppLogger + end + + def unauthorized_to_create + raise SignupDisabledError + end + end + end +end diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb deleted file mode 100644 index 47f62153a50..00000000000 --- a/lib/gitlab/oauth/user.rb +++ /dev/null @@ -1,107 +0,0 @@ -# OAuth extension for User model -# -# * Find GitLab user based on omniauth uid and provider -# * Create new user from omniauth data -# -module Gitlab - module OAuth - class User - attr_accessor :auth_hash, :gl_user - - def initialize(auth_hash) - self.auth_hash = auth_hash - end - - def persisted? - gl_user.try(:persisted?) - end - - def new? - !persisted? - end - - def valid? - gl_user.try(:valid?) - end - - def save - unauthorized_to_create unless gl_user - - if needs_blocking? - gl_user.save! - gl_user.block - else - gl_user.save! - end - - log.info "(OAuth) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" - gl_user - rescue ActiveRecord::RecordInvalid => e - log.info "(OAuth) Error saving user: #{gl_user.errors.full_messages}" - return self, e.record.errors - end - - def gl_user - @user ||= find_by_uid_and_provider - - if signup_enabled? - @user ||= build_new_user - end - - @user - end - - protected - - def needs_blocking? - new? && block_after_signup? - end - - def signup_enabled? - Gitlab.config.omniauth.allow_single_sign_on - end - - def block_after_signup? - Gitlab.config.omniauth.block_auto_created_users - end - - def auth_hash=(auth_hash) - @auth_hash = AuthHash.new(auth_hash) - end - - def find_by_uid_and_provider - model.where(provider: auth_hash.provider, extern_uid: auth_hash.uid).last - end - - def build_new_user - model.new(user_attributes).tap do |user| - user.skip_confirmation! - end - end - - def user_attributes - { - extern_uid: auth_hash.uid, - provider: auth_hash.provider, - name: auth_hash.name, - username: auth_hash.username, - email: auth_hash.email, - password: auth_hash.password, - password_confirmation: auth_hash.password, - } - end - - def log - Gitlab::AppLogger - end - - def model - ::User - end - - def raise_unauthorized_to_create - raise StandardError.new("Unauthorized to create user, signup disabled for #{auth_hash.provider}") - end - end - end -end diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb index e2fbafb3899..43e07e09160 100644 --- a/lib/gitlab/popen.rb +++ b/lib/gitlab/popen.rb @@ -21,12 +21,15 @@ module Gitlab @cmd_output = "" @cmd_status = 0 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + # We are not using stdin so we should close it, in case the command we + # are running waits for input. + stdin.close @cmd_output << stdout.read @cmd_output << stderr.read @cmd_status = wait_thr.value.exitstatus end - return @cmd_output, @cmd_status + [@cmd_output, @cmd_status] end end end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 8b85f3da83f..0dab7bcfa4d 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -67,7 +67,7 @@ module Gitlab end def notes - Note.where(project_id: limit_project_ids).search(query).order('updated_at DESC') + Note.where(project_id: limit_project_ids).user.search(query).order('updated_at DESC') end def limit_project_ids diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb new file mode 100644 index 00000000000..d010ade704e --- /dev/null +++ b/lib/gitlab/push_data_builder.rb @@ -0,0 +1,91 @@ +module Gitlab + class PushDataBuilder + class << self + # Produce a hash of post-receive data + # + # data = { + # before: String, + # after: String, + # ref: String, + # user_id: String, + # user_name: String, + # user_email: String + # project_id: String, + # repository: { + # name: String, + # url: String, + # description: String, + # homepage: String, + # }, + # commits: Array, + # total_commits_count: Fixnum + # } + # + def build(project, user, oldrev, newrev, ref, commits = [], message = nil) + # Total commits count + commits_count = commits.size + + # Get latest 20 commits ASC + commits_limited = commits.last(20) + + # For performance purposes maximum 20 latest commits + # will be passed as post receive hook data. + commit_attrs = commits_limited.map(&:hook_attrs) + + type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push" + # Hash to be passed as post_receive_data + data = { + object_kind: type, + before: oldrev, + after: newrev, + ref: ref, + checkout_sha: checkout_sha(project.repository, newrev, ref), + message: message, + user_id: user.id, + user_name: user.name, + user_email: user.email, + project_id: project.id, + repository: { + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url, + git_http_url: project.http_url_to_repo, + git_ssh_url: project.ssh_url_to_repo, + visibility_level: project.visibility_level + }, + commits: commit_attrs, + total_commits_count: commits_count + } + + data + end + + # This method provide a sample data generated with + # existing project and commits to test web hooks + def build_sample(project, user) + commits = project.repository.commits(project.default_branch, nil, 3) + ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}" + build(project, user, commits.last.id, commits.first.id, ref, commits) + end + + def checkout_sha(repository, newrev, ref) + # Checkout sha is nil when we remove branch or tag + return if Gitlab::Git.blank_ref?(newrev) + + # Find sha for tag, except when it was deleted. + if Gitlab::Git.tag_ref?(ref) + tag_name = Gitlab::Git.ref_name(ref) + tag = repository.find_tag(tag_name) + + if tag + commit = repository.commit(tag.target) + commit.try(:sha) + end + else + newrev + end + end + end + end +end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 99165950aef..e836b05ff25 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -1,71 +1,61 @@ module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor - attr_accessor :users, :issues, :merge_requests, :snippets, :commits + attr_accessor :project, :current_user - include Markdown - - def initialize - @users, @issues, @merge_requests, @snippets, @commits = [], [], [], [], [] - end - - def analyze(string, project) - parse_references(string.dup, project) - end - - # Given a valid project, resolve the extracted identifiers of the requested type to - # model objects. - - def users_for(project) - users.map do |entry| - project.users.where(username: entry[:id]).first - end.reject(&:nil?) + def initialize(project, current_user = nil) + @project = project + @current_user = current_user end - def issues_for(project = nil) - issues.map do |entry| - if should_lookup?(project, entry[:project]) - entry[:project].issues.where(iid: entry[:id]).first - end - end.reject(&:nil?) + def analyze(text) + references.clear + @text = markdown.render(text.dup) end - def merge_requests_for(project = nil) - merge_requests.map do |entry| - if should_lookup?(project, entry[:project]) - entry[:project].merge_requests.where(iid: entry[:id]).first - end - end.reject(&:nil?) + %i(user label issue merge_request snippet commit commit_range).each do |type| + define_method("#{type}s") do + references[type] + end end - def snippets_for(project) - snippets.map do |entry| - project.snippets.where(id: entry[:id]).first - end.reject(&:nil?) - end + private - def commits_for(project = nil) - commits.map do |entry| - repo = entry[:project].repository if entry[:project] - if should_lookup?(project, entry[:project]) - repo.commit(entry[:id]) if repo - end - end.reject(&:nil?) + def markdown + @markdown ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, GitlabMarkdownHelper::MARKDOWN_OPTIONS) end - private + def references + @references ||= Hash.new do |references, type| + type = type.to_sym + return references[type] if references.has_key?(type) - def reference_link(type, identifier, project, _) - # Append identifier to the appropriate collection. - send("#{type}s") << { project: project, id: identifier } + references[type] = pipeline_result(type).uniq + end end - def should_lookup?(project, entry_project) - if entry_project.nil? - false - else - project.nil? || project.id == entry_project.id - end + # Instantiate and call HTML::Pipeline with a single reference filter type, + # returning the result + # + # filter_type - Symbol reference type (e.g., :commit, :issue, etc.) + # + # Returns the results Array for the requested filter type + def pipeline_result(filter_type) + klass = filter_type.to_s.camelize + 'ReferenceFilter' + filter = "Gitlab::Markdown::#{klass}".constantize + + context = { + project: project, + current_user: current_user, + # We don't actually care about the links generated + only_path: true, + ignore_blockquotes: true + } + + pipeline = HTML::Pipeline.new([filter], context) + result = pipeline.call(@text) + + result[:references][filter_type] end end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index c4d0d85b7f5..9f1adc860d1 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -2,49 +2,66 @@ module Gitlab module Regex extend self - def username_regex - default_regex + NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])'.freeze + + def namespace_regex + @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze + end + + def namespace_regex_message + "can contain only letters, digits, '_', '-' and '.'. " \ + "Cannot start with '-' or end in '.'." \ + end + + + def namespace_name_regex + @namespace_name_regex ||= /\A[\p{Alnum}\p{Pd}_\. ]*\z/.freeze end - def username_regex_message - default_regex_message + def namespace_name_regex_message + "can contain only letters, digits, '_', '.', dash and space." end + def project_name_regex - /\A[a-zA-Z0-9_][a-zA-Z0-9_\-\. ]*\z/ + @project_name_regex ||= /\A[\p{Alnum}_][\p{Alnum}\p{Pd}_\. ]*\z/.freeze end - def project_regex_message - "can contain only letters, digits, '_', '-' and '.' and space. " \ + def project_name_regex_message + "can contain only letters, digits, '_', '.', dash and space. " \ "It must start with letter, digit or '_'." end - def name_regex - /\A[a-zA-Z0-9_\-\. ]*\z/ + + def project_path_regex + @project_path_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(?<!\.git)\z/.freeze end - def name_regex_message - "can contain only letters, digits, '_', '-' and '.' and space." + def project_path_regex_message + "can contain only letters, digits, '_', '-' and '.'. " \ + "Cannot start with '-' or end in '.git'" \ end - def path_regex - default_regex + + def file_name_regex + @file_name_regex ||= /\A[a-zA-Z0-9_\-\.]*\z/.freeze end - def path_regex_message - default_regex_message + def file_name_regex_message + "can contain only letters, digits, '_', '-' and '.'. " end + def archive_formats_regex - #|zip|tar| tar.gz | tar.bz2 | - /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/ + # |zip|tar| tar.gz | tar.bz2 | + @archive_formats_regex ||= /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/.freeze end def git_reference_regex # Valid git ref regex, see: # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html - %r{ + @git_reference_regex ||= %r{ (?! (?# doesn't begins with) \/| (?# rule #6) @@ -60,18 +77,7 @@ module Gitlab (?# doesn't end with) (?<!\.lock) (?# rule #1) (?<![\/.]) (?# rule #6-7) - }x - end - - protected - - def default_regex_message - "can contain only letters, digits, '_', '-' and '.'. " \ - "Cannot start with '-' or end in '.git'" \ - end - - def default_regex - /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(?<!\.git)\z/ + }x.freeze end end end diff --git a/lib/gitlab/satellite/action.rb b/lib/gitlab/satellite/action.rb index be45cb5c98e..4890ccf21e6 100644 --- a/lib/gitlab/satellite/action.rb +++ b/lib/gitlab/satellite/action.rb @@ -44,7 +44,7 @@ module Gitlab end def default_options(options = {}) - {raise: true, timeout: true}.merge(options) + { raise: true, timeout: true }.merge(options) end def handle_exception(exception) diff --git a/lib/gitlab/satellite/files/delete_file_action.rb b/lib/gitlab/satellite/files/delete_file_action.rb deleted file mode 100644 index 30462999aa3..00000000000 --- a/lib/gitlab/satellite/files/delete_file_action.rb +++ /dev/null @@ -1,50 +0,0 @@ -require_relative 'file_action' - -module Gitlab - module Satellite - class DeleteFileAction < FileAction - # Deletes file and creates a new commit for it - # - # Returns false if committing the change fails - # Returns false if pushing from the satellite to bare repo failed or was rejected - # Returns true otherwise - def commit!(content, commit_message) - in_locked_and_timed_satellite do |repo| - prepare_satellite!(repo) - - # create target branch in satellite at the corresponding commit from bare repo - repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") - - # update the file in the satellite's working dir - file_path_in_satellite = File.join(repo.working_dir, file_path) - - # Prevent relative links - unless safe_path?(file_path_in_satellite) - Gitlab::GitLogger.error("FileAction: Relative path not allowed") - return false - end - - File.delete(file_path_in_satellite) - - # add removed file - repo.remove(file_path_in_satellite) - - # commit the changes - # will raise CommandFailed when commit fails - repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) - - - # push commit back to bare repo - # will raise CommandFailed when push fails - repo.git.push({raise: true, timeout: true}, :origin, ref) - - # everything worked - true - end - rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false - end - end - end -end diff --git a/lib/gitlab/satellite/files/edit_file_action.rb b/lib/gitlab/satellite/files/edit_file_action.rb deleted file mode 100644 index cbdf70f7d12..00000000000 --- a/lib/gitlab/satellite/files/edit_file_action.rb +++ /dev/null @@ -1,50 +0,0 @@ -require_relative 'file_action' - -module Gitlab - module Satellite - # GitLab server-side file update and commit - class EditFileAction < FileAction - # Updates the files content and creates a new commit for it - # - # Returns false if the ref has been updated while editing the file - # Returns false if committing the change fails - # Returns false if pushing from the satellite to bare repo failed or was rejected - # Returns true otherwise - def commit!(content, commit_message, encoding) - in_locked_and_timed_satellite do |repo| - prepare_satellite!(repo) - - # create target branch in satellite at the corresponding commit from bare repo - repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") - - # update the file in the satellite's working dir - file_path_in_satellite = File.join(repo.working_dir, file_path) - - # Prevent relative links - unless safe_path?(file_path_in_satellite) - Gitlab::GitLogger.error("FileAction: Relative path not allowed") - return false - end - - # Write file - write_file(file_path_in_satellite, content, encoding) - - # commit the changes - # will raise CommandFailed when commit fails - repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) - - - # push commit back to bare repo - # will raise CommandFailed when push fails - repo.git.push({raise: true, timeout: true}, :origin, ref) - - # everything worked - true - end - rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false - end - end - end -end diff --git a/lib/gitlab/satellite/files/file_action.rb b/lib/gitlab/satellite/files/file_action.rb deleted file mode 100644 index 6446b14568a..00000000000 --- a/lib/gitlab/satellite/files/file_action.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Gitlab - module Satellite - class FileAction < Action - attr_accessor :file_path, :ref - - def initialize(user, project, ref, file_path) - super user, project - @file_path = file_path - @ref = ref - end - - def safe_path?(path) - File.absolute_path(path) == path - end - - def write_file(abs_file_path, content, file_encoding = 'text') - if file_encoding == 'base64' - File.open(abs_file_path, 'wb') { |f| f.write(Base64.decode64(content)) } - else - File.open(abs_file_path, 'w') { |f| f.write(content) } - end - end - end - end -end diff --git a/lib/gitlab/satellite/files/new_file_action.rb b/lib/gitlab/satellite/files/new_file_action.rb deleted file mode 100644 index 15e9b7a6f77..00000000000 --- a/lib/gitlab/satellite/files/new_file_action.rb +++ /dev/null @@ -1,55 +0,0 @@ -require_relative 'file_action' - -module Gitlab - module Satellite - class NewFileAction < FileAction - # Updates the files content and creates a new commit for it - # - # Returns false if the ref has been updated while editing the file - # Returns false if committing the change fails - # Returns false if pushing from the satellite to bare repo failed or was rejected - # Returns true otherwise - def commit!(content, commit_message, encoding) - in_locked_and_timed_satellite do |repo| - prepare_satellite!(repo) - - # create target branch in satellite at the corresponding commit from bare repo - repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") - - file_path_in_satellite = File.join(repo.working_dir, file_path) - dir_name_in_satellite = File.dirname(file_path_in_satellite) - - # Prevent relative links - unless safe_path?(file_path_in_satellite) - Gitlab::GitLogger.error("FileAction: Relative path not allowed") - return false - end - - # Create dir if not exists - FileUtils.mkdir_p(dir_name_in_satellite) - - # Write file - write_file(file_path_in_satellite, content, encoding) - - # add new file - repo.add(file_path_in_satellite) - - # commit the changes - # will raise CommandFailed when commit fails - repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) - - - # push commit back to bare repo - # will raise CommandFailed when push fails - repo.git.push({raise: true, timeout: true}, :origin, ref) - - # everything worked - true - end - rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false - end - end - end -end diff --git a/lib/gitlab/satellite/merge_action.rb b/lib/gitlab/satellite/merge_action.rb index e9141f735aa..1f2e5f82dd5 100644 --- a/lib/gitlab/satellite/merge_action.rb +++ b/lib/gitlab/satellite/merge_action.rb @@ -86,7 +86,7 @@ module Gitlab in_locked_and_timed_satellite do |merge_repo| prepare_satellite!(merge_repo) update_satellite_source_and_target!(merge_repo) - patch = merge_repo.git.format_patch(default_options({stdout: true}), "origin/#{merge_request.target_branch}..source/#{merge_request.source_branch}") + patch = merge_repo.git.format_patch(default_options({ stdout: true }), "origin/#{merge_request.target_branch}..source/#{merge_request.source_branch}") end rescue Grit::Git::CommandFailed => ex handle_exception(ex) @@ -97,7 +97,7 @@ module Gitlab in_locked_and_timed_satellite do |merge_repo| prepare_satellite!(merge_repo) update_satellite_source_and_target!(merge_repo) - if (merge_request.for_fork?) + if merge_request.for_fork? repository = Gitlab::Git::Repository.new(merge_repo.path) commits = Gitlab::Git::Commit.between( repository, @@ -128,7 +128,7 @@ module Gitlab # merge the source branch into the satellite # will raise CommandFailed when merge fails - repo.git.merge(default_options({no_ff: true}), "-m#{message}", "source/#{merge_request.source_branch}") + repo.git.merge(default_options({ no_ff: true }), "-m#{message}", "source/#{merge_request.source_branch}") rescue Grit::Git::CommandFailed => ex handle_exception(ex) end @@ -137,7 +137,7 @@ module Gitlab def update_satellite_source_and_target!(repo) repo.remote_add('source', merge_request.source_project.repository.path_to_repo) repo.remote_fetch('source') - repo.git.checkout(default_options({b: true}), merge_request.target_branch, "origin/#{merge_request.target_branch}") + repo.git.checkout(default_options({ b: true }), merge_request.target_branch, "origin/#{merge_request.target_branch}") rescue Grit::Git::CommandFailed => ex handle_exception(ex) end diff --git a/lib/gitlab/satellite/satellite.rb b/lib/gitlab/satellite/satellite.rb index 1de84309d15..398643d68de 100644 --- a/lib/gitlab/satellite/satellite.rb +++ b/lib/gitlab/satellite/satellite.rb @@ -1,5 +1,14 @@ module Gitlab module Satellite + autoload :DeleteFileAction, 'gitlab/satellite/files/delete_file_action' + autoload :EditFileAction, 'gitlab/satellite/files/edit_file_action' + autoload :FileAction, 'gitlab/satellite/files/file_action' + autoload :NewFileAction, 'gitlab/satellite/files/new_file_action' + + class CheckoutFailed < StandardError; end + class CommitFailed < StandardError; end + class PushFailed < StandardError; end + class Satellite include Gitlab::Popen @@ -95,16 +104,12 @@ module Gitlab heads = repo.heads.map(&:name) # update or create the parking branch - if heads.include? PARKING_BRANCH - repo.git.checkout({}, PARKING_BRANCH) - else - repo.git.checkout(default_options({b: true}), PARKING_BRANCH) - end + repo.git.checkout(default_options({ B: true }), PARKING_BRANCH) # remove the parking branch from the list of heads ... heads.delete(PARKING_BRANCH) # ... and delete all others - heads.each { |head| repo.git.branch(default_options({D: true}), head) } + heads.each { |head| repo.git.branch(default_options({ D: true }), head) } end # Deletes all remotes except origin @@ -126,7 +131,7 @@ module Gitlab end def default_options(options = {}) - {raise: true, timeout: true}.merge(options) + { raise: true, timeout: true }.merge(options) end # Create directory for storing diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 75a3dfe37c3..06245374bc8 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -51,11 +51,23 @@ module Gitlab end def issues - Issue.where(project_id: limit_project_ids).full_search(query).order('updated_at DESC') + issues = Issue.where(project_id: limit_project_ids) + if query =~ /#(\d+)\z/ + issues = issues.where(iid: $1) + else + issues = issues.full_search(query) + end + issues.order('updated_at DESC') end def merge_requests - MergeRequest.in_projects(limit_project_ids).full_search(query).order('updated_at DESC') + merge_requests = MergeRequest.in_projects(limit_project_ids) + if query =~ /[#!](\d+)\z/ + merge_requests = merge_requests.where(iid: $1) + else + merge_requests = merge_requests.full_search(query) + end + merge_requests.order('updated_at DESC') end def default_scope diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb index 0fb09d3f228..37232743325 100644 --- a/lib/gitlab/sidekiq_middleware/memory_killer.rb +++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb @@ -1,27 +1,43 @@ module Gitlab module SidekiqMiddleware class MemoryKiller + # Default the RSS limit to 0, meaning the MemoryKiller is disabled + MAX_RSS = (ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] || 0).to_s.to_i + # Give Sidekiq 15 minutes of grace time after exceeding the RSS limit + GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i # Wait 30 seconds for running jobs to finish during graceful shutdown - GRACEFUL_SHUTDOWN_WAIT = 30 + SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i + SHUTDOWN_SIGNAL = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL'] || 'SIGKILL').to_s + + # Create a mutex used to ensure there will be only one thread waiting to + # shut Sidekiq down + MUTEX = Mutex.new def call(worker, job, queue) yield current_rss = get_rss - return unless max_rss > 0 && current_rss > max_rss - - Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\ - "#{max_rss}" - Sidekiq.logger.warn "sending SIGUSR1 to PID #{Process.pid}" - # SIGUSR1 tells Sidekiq to stop accepting new jobs - Process.kill('SIGUSR1', Process.pid) - - Sidekiq.logger.warn "spawning thread that will send SIGTERM to PID "\ - "#{Process.pid} in #{graceful_shutdown_wait} seconds" - # Send the final shutdown signal to Sidekiq from a separate thread so - # that the current job can finish + + return unless MAX_RSS > 0 && current_rss > MAX_RSS + Thread.new do - sleep(graceful_shutdown_wait) - Process.kill('SIGTERM', Process.pid) + # Return if another thread is already waiting to shut Sidekiq down + return unless MUTEX.try_lock + + Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\ + "#{MAX_RSS}" + Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} "\ + "in #{GRACE_TIME} seconds" + sleep(GRACE_TIME) + + Sidekiq.logger.warn "sending SIGUSR1 to PID #{Process.pid}" + Process.kill('SIGUSR1', Process.pid) + + Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\ + "#{SHUTDOWN_SIGNAL} to PID #{Process.pid}" + sleep(SHUTDOWN_WAIT) + + Sidekiq.logger.warn "sending #{SHUTDOWN_SIGNAL} to PID #{Process.pid}" + Process.kill(SHUTDOWN_SIGNAL, Process.pid) end end @@ -33,16 +49,6 @@ module Gitlab output.to_i end - - def max_rss - @max_rss ||= ENV['SIDEKIQ_MAX_RSS'].to_s.to_i - end - - def graceful_shutdown_wait - @graceful_shutdown_wait ||= ( - ENV['SIDEKIQ_GRACEFUL_SHUTDOWN_WAIT'] || GRACEFUL_SHUTDOWN_WAIT - ).to_i - end end end end diff --git a/lib/gitlab/theme.rb b/lib/gitlab/theme.rb deleted file mode 100644 index b7c50cb734d..00000000000 --- a/lib/gitlab/theme.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Gitlab - class Theme - BASIC = 1 unless const_defined?(:BASIC) - MARS = 2 unless const_defined?(:MARS) - MODERN = 3 unless const_defined?(:MODERN) - GRAY = 4 unless const_defined?(:GRAY) - COLOR = 5 unless const_defined?(:COLOR) - - def self.css_class_by_id(id) - themes = { - BASIC => "ui_basic", - MARS => "ui_mars", - MODERN => "ui_modern", - GRAY => "ui_gray", - COLOR => "ui_color" - } - - id ||= Gitlab.config.gitlab.default_theme - - return themes[id] - end - end -end diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb new file mode 100644 index 00000000000..5209df92795 --- /dev/null +++ b/lib/gitlab/themes.rb @@ -0,0 +1,67 @@ +module Gitlab + # Module containing GitLab's application theme definitions and helper methods + # for accessing them. + module Themes + # Theme ID used when no `default_theme` configuration setting is provided. + APPLICATION_DEFAULT = 2 + + # Struct class representing a single Theme + Theme = Struct.new(:id, :name, :css_class) + + # All available Themes + THEMES = [ + Theme.new(1, 'Graphite', 'ui_graphite'), + Theme.new(2, 'Charcoal', 'ui_charcoal'), + Theme.new(3, 'Green', 'ui_green'), + Theme.new(4, 'Gray', 'ui_gray'), + Theme.new(5, 'Violet', 'ui_violet'), + Theme.new(6, 'Blue', 'ui_blue') + ].freeze + + # Convenience method to get a space-separated String of all the theme + # classes that might be applied to the `body` element + # + # Returns a String + def self.body_classes + THEMES.collect(&:css_class).uniq.join(' ') + end + + # Get a Theme by its ID + # + # If the ID is invalid, returns the default Theme. + # + # id - Integer ID + # + # Returns a Theme + def self.by_id(id) + THEMES.detect { |t| t.id == id } || default + end + + # Get the default Theme + # + # Returns a Theme + def self.default + by_id(default_id) + end + + # Iterate through each Theme + # + # Yields the Theme object + def self.each(&block) + THEMES.each(&block) + end + + private + + def self.default_id + id = Gitlab.config.gitlab.default_theme.to_i + + # Prevent an invalid configuration setting from causing an infinite loop + if id < THEMES.first.id || id > THEMES.last.id + APPLICATION_DEFAULT + else + id + end + end + end +end diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb index 74b049b5143..cf040971c6e 100644 --- a/lib/gitlab/upgrader.rb +++ b/lib/gitlab/upgrader.rb @@ -43,10 +43,15 @@ module Gitlab end def latest_version_raw + git_tags = fetch_git_tags + git_tags = git_tags.select { |version| version =~ /v\d+\.\d+\.\d+\Z/ } + git_versions = git_tags.map { |tag| Gitlab::VersionInfo.parse(tag.match(/v\d+\.\d+\.\d+/).to_s) } + "v#{git_versions.sort.last.to_s}" + end + + def fetch_git_tags remote_tags, _ = Gitlab::Popen.popen(%W(git ls-remote --tags https://gitlab.com/gitlab-org/gitlab-ce.git)) - git_tags = remote_tags.split("\n").grep(/tags\/v#{current_version.major}/) - git_tags = git_tags.select { |version| version =~ /v\d\.\d\.\d\Z/ } - last_tag = git_tags.last.match(/v\d\.\d\.\d/).to_s + remote_tags.split("\n").grep(/tags\/v#{current_version.major}/) end def update_commands @@ -62,7 +67,7 @@ module Gitlab end def env - {'RAILS_ENV' => 'production'} + { 'RAILS_ENV' => 'production' } end def upgrade diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 877488d8471..11b0d44f340 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -1,6 +1,7 @@ module Gitlab class UrlBuilder include Rails.application.routes.url_helpers + include GitlabRoutingHelper def initialize(type) @type = type @@ -9,17 +10,51 @@ module Gitlab def build(id) case @type when :issue - issue_url(id) + build_issue_url(id) + when :merge_request + build_merge_request_url(id) + when :note + build_note_url(id) + end end private - def issue_url(id) + def build_issue_url(id) issue = Issue.find(id) - project_issue_url(id: issue.iid, - project_id: issue.project, - host: Gitlab.config.gitlab['url']) + issue_url(issue, host: Gitlab.config.gitlab['url']) + end + + def build_merge_request_url(id) + merge_request = MergeRequest.find(id) + merge_request_url(merge_request, host: Gitlab.config.gitlab['url']) + end + + def build_note_url(id) + note = Note.find(id) + if note.for_commit? + namespace_project_commit_url(namespace_id: note.project.namespace, + id: note.commit_id, + project_id: note.project, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + elsif note.for_issue? + issue = Issue.find(note.noteable_id) + issue_url(issue, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + elsif note.for_merge_request? + merge_request = MergeRequest.find(note.noteable_id) + merge_request_url(merge_request, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + elsif note.for_project_snippet? + snippet = Snippet.find(note.noteable_id) + project_snippet_url(snippet, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + end end end end diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index bd184c27187..d13fe0ef8a9 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -9,5 +9,9 @@ module Gitlab def system_silent(cmd) Popen::popen(cmd).last.zero? end + + def force_utf8(str) + str.force_encoding(Encoding::UTF_8) + end end end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index d0b6cde3c7e..582fc759efd 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -5,6 +5,8 @@ # module Gitlab module VisibilityLevel + extend CurrentSettings + PRIVATE = 0 unless const_defined?(:PRIVATE) INTERNAL = 10 unless const_defined?(:INTERNAL) PUBLIC = 20 unless const_defined?(:PUBLIC) @@ -23,21 +25,27 @@ module Gitlab end def allowed_for?(user, level) - user.is_admin? || allowed_level?(level) + user.is_admin? || allowed_level?(level.to_i) end - # Level can be a string `"public"` or a value `20`, first check if valid, - # then check if the corresponding string appears in the config + # Return true if the specified level is allowed for the current user. + # Level should be a numeric value, e.g. `20`. def allowed_level?(level) - if options.has_key?(level.to_s) - non_restricted_level?(level) - elsif options.has_value?(level.to_i) - non_restricted_level?(options.key(level.to_i).downcase) - end + valid_level?(level) && non_restricted_level?(level) end def non_restricted_level?(level) - ! Gitlab.config.gitlab.restricted_visibility_levels.include?(level) + restricted_levels = current_application_settings.restricted_visibility_levels + + if restricted_levels.nil? + true + else + !restricted_levels.include?(level) + end + end + + def valid_level?(level) + options.has_value?(level) end end diff --git a/lib/omni_auth/request_forgery_protection.rb b/lib/omni_auth/request_forgery_protection.rb new file mode 100644 index 00000000000..3557522d3c9 --- /dev/null +++ b/lib/omni_auth/request_forgery_protection.rb @@ -0,0 +1,66 @@ +# Protects OmniAuth request phase against CSRF. + +module OmniAuth + # Based on ActionController::RequestForgeryProtection. + class RequestForgeryProtection + def initialize(env) + @env = env + end + + def request + @request ||= ActionDispatch::Request.new(@env) + end + + def session + request.session + end + + def reset_session + request.reset_session + end + + def params + request.params + end + + def call + verify_authenticity_token + end + + def verify_authenticity_token + if !verified_request? + Rails.logger.warn "Can't verify CSRF token authenticity" if Rails.logger + handle_unverified_request + end + end + + private + + def protect_against_forgery? + ApplicationController.allow_forgery_protection + end + + def request_forgery_protection_token + ApplicationController.request_forgery_protection_token + end + + def forgery_protection_strategy + ApplicationController.forgery_protection_strategy + end + + def verified_request? + !protect_against_forgery? || request.get? || request.head? || + form_authenticity_token == params[request_forgery_protection_token] || + form_authenticity_token == request.headers['X-CSRF-Token'] + end + + def handle_unverified_request + forgery_protection_strategy.new(self).handle_unverified_request + end + + # Sets the token value for the current session. + def form_authenticity_token + session[:_csrf_token] ||= SecureRandom.base64(32) + end + end +end diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 54d740908d5..2f7aff03c2a 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -1,68 +1,46 @@ -class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML +require 'active_support/core_ext/string/output_safety' +class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML attr_reader :template alias_method :h, :template - def initialize(template, options = {}) + def initialize(template, color_scheme, options = {}) @template = template - @project = @template.instance_variable_get("@project") + @color_scheme = color_scheme @options = options.dup - super options + + @options.reverse_merge!( + # Handled further down the line by Gitlab::Markdown::SanitizationFilter + escape_html: false, + project: @template.instance_variable_get("@project") + ) + + super(options) end - # If project has issue number 39, apostrophe will be linked in - # regular text to the issue as Redcarpet will convert apostrophe to - # #39; - # We replace apostrophe with right single quote before Redcarpet - # does the processing and put the apostrophe back in postprocessing. - # This only influences regular text, code blocks are untouched. def normal_text(text) - return text unless text.present? - text.gsub("'", "’") + ERB::Util.html_escape_once(text) end + # Stolen from Rugments::Plugins::Redcarpet as this module is not required + # from Rugments's gem root. def block_code(code, language) - # New lines are placed to fix an rendering issue - # with code wrapped inside <h1> tag for next case: - # - # # Title kinda h1 - # - # ruby code here - # - <<-HTML - -<div class="highlighted-data #{h.user_color_scheme_class}"> - <div class="highlight"> - <pre><code class="#{language}">#{h.send(:html_escape, code)}</code></pre> - </div> -</div> - - HTML - end - - def link(link, title, content) - h.link_to_gfm(content, link, title: title) - end + lexer = Rugments::Lexer.find_fancy(language, code) || Rugments::Lexers::PlainText - def header(text, level) - if @options[:no_header_anchors] - "<h#{level}>#{text}</h#{level}>" - else - id = ActionController::Base.helpers.strip_tags(h.gfm(text)).downcase() \ - .gsub(/[^a-z0-9_-]/, '-').gsub(/-+/, '-').gsub(/^-/, '').gsub(/-$/, '') - "<h#{level} id=\"#{id}\">#{text}<a href=\"\##{id}\"></a></h#{level}>" + # XXX HACK: Redcarpet strips hard tabs out of code blocks, + # so we assume you're not using leading spaces that aren't tabs, + # and just replace them here. + if lexer.tag == 'make' + code.gsub!(/^ /, "\t") end + + formatter = Rugments::Formatters::HTML.new( + cssclass: "code highlight #{@color_scheme} #{lexer.tag}" + ) + formatter.format(lexer.lex(code)) end def postprocess(full_document) - full_document.gsub!("’", "'") - unless @template.instance_variable_get("@project_wiki") || @project.nil? - full_document = h.create_relative_links(full_document) - end - if @options[:parse_tasks] - h.gfm_with_tasks(full_document) - else - h.gfm(full_document) - end + h.gfm_with_options(full_document, @options) end end diff --git a/lib/repository_cache.rb b/lib/repository_cache.rb new file mode 100644 index 00000000000..fa016a170cd --- /dev/null +++ b/lib/repository_cache.rb @@ -0,0 +1,21 @@ +# Interface to the Redis-backed cache store used by the Repository model +class RepositoryCache + attr_reader :namespace, :backend + + def initialize(namespace, backend = Rails.cache) + @namespace = namespace + @backend = backend + end + + def cache_key(type) + "#{type}:#{namespace}" + end + + def expire(key) + backend.delete(cache_key(key)) + end + + def fetch(key, &block) + backend.fetch(cache_key(key), &block) + end +end diff --git a/lib/support/deploy/deploy.sh b/lib/support/deploy/deploy.sh index 4684957233a..adea4c7a747 100755 --- a/lib/support/deploy/deploy.sh +++ b/lib/support/deploy/deploy.sh @@ -4,7 +4,7 @@ # If any command return non-zero status - stop deploy set -e -echo 'Deploy: Stoping sidekiq..' +echo 'Deploy: Stopping sidekiq..' cd /home/git/gitlab/ && sudo -u git -H bundle exec rake sidekiq:stop RAILS_ENV=production echo 'Deploy: Show deploy index page' diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index b066a1a6935..946902e2f6d 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -35,13 +35,14 @@ pid_path="$app_root/tmp/pids" socket_path="$app_root/tmp/sockets" web_server_pid_path="$pid_path/unicorn.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" +shell_path="/bin/bash" # Read configuration variable file if it is present test -f /etc/default/gitlab && . /etc/default/gitlab # Switch to the app_user if it is not he/she who is running the script. if [ "$USER" != "$app_user" ]; then - eval su - "$app_user" -c $(echo \")$0 "$@"$(echo \"); exit; + eval su - "$app_user" -s $shell_path -c $(echo \")$0 "$@"$(echo \"); exit; fi # Switch to the gitlab path, exit on failure. diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index 9951bacedf5..cf7f4198cbf 100755 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -29,3 +29,8 @@ web_server_pid_path="$pid_path/unicorn.pid" # sidekiq_pid_path defines the path in which to create the pid file for sidekiq # The default is "$pid_path/sidekiq.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" + +# shell_path defines the path of shell for "$app_user" in case you are using +# shell other than "bash" +# The default is "/bin/bash" +shell_path="/bin/bash" diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index c25c269148f..edb987875df 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -1,10 +1,16 @@ ## GitLab -## Contributors: randx, yin8086, sashkab, orkoden, axilleas, bbodenmiller ## ## Lines starting with two hashes (##) are comments with information. ## Lines starting with one hash (#) are configuration parameters that can be uncommented. ## ################################## +## CONTRIBUTING ## +################################## +## +## If you change this file in a Merge Request, please also create +## a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests +## +################################## ## CHUNKED TRANSFER ## ################################## ## @@ -60,6 +66,27 @@ server { try_files $uri $uri/index.html $uri.html @gitlab; } + ## We route uploads through GitLab to prevent XSS and enforce access control. + location /uploads/ { + ## If you use HTTPS make sure you disable gzip compression + ## to be safe against BREACH attack. + # gzip off; + + ## https://github.com/gitlabhq/gitlabhq/issues/694 + ## Some requests take more than 30 seconds. + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_redirect off; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; + + proxy_pass http://gitlab; + } + ## If a file, which is not found in the root folder is requested, ## then the proxy passes the request to the upsteam (gitlab unicorn). location @gitlab { diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 25b52ee17da..766559b49f6 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -1,5 +1,4 @@ ## GitLab -## Contributors: randx, yin8086, sashkab, orkoden, axilleas, bbodenmiller ## ## Modified from nginx http version ## Modified from http://blog.phusion.nl/2012/04/21/tutorial-setting-up-gitlab-on-debian-6/ @@ -9,6 +8,13 @@ ## Lines starting with one hash (#) are configuration parameters that can be uncommented. ## ################################## +## CONTRIBUTING ## +################################## +## +## If you change this file in a Merge Request, please also create +## a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests +## +################################## ## CHUNKED TRANSFER ## ################################## ## @@ -43,7 +49,7 @@ server { ## to be served if you visit any address that your server responds to, eg. ## the ip address of the server (http://x.x.x.x/) listen 0.0.0.0:80; - listen [::]:80 default_server; + listen [::]:80 ipv6only=on default_server; server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com server_tokens off; ## Don't show the nginx version number, a security best practice return 301 https://$server_name$request_uri; @@ -55,7 +61,7 @@ server { ## HTTPS host server { listen 0.0.0.0:443 ssl; - listen [::]:443 ssl default_server; + listen [::]:443 ipv6only=on ssl default_server; server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com server_tokens off; ## Don't show the nginx version number, a security best practice root /home/git/gitlab/public; @@ -71,7 +77,7 @@ server { ssl_certificate_key /etc/nginx/ssl/gitlab.key; # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs - ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + 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 shared:SSL:10m; @@ -105,6 +111,28 @@ server { try_files $uri $uri/index.html $uri.html @gitlab; } + ## We route uploads through GitLab to prevent XSS and enforce access control. + location /uploads/ { + ## If you use HTTPS make sure you disable gzip compression + ## to be safe against BREACH attack. + gzip off; + + ## https://github.com/gitlabhq/gitlabhq/issues/694 + ## Some requests take more than 30 seconds. + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_redirect off; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Ssl on; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; + + proxy_pass http://gitlab; + } + ## If a file, which is not found in the root folder is requested, ## then the proxy passes the request to the upsteam (gitlab unicorn). location @gitlab { diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake new file mode 100644 index 00000000000..5d4e0740373 --- /dev/null +++ b/lib/tasks/brakeman.rake @@ -0,0 +1,11 @@ +desc 'Security check via brakeman' +task :brakeman do + # We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge + # requests are welcome! + if system(*%W(brakeman --skip-files lib/backup/repository.rb -w3 -z)) + puts 'Security check succeed' + else + puts 'Security check failed' + exit 1 + end +end diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index 058c7417040..b22c631c8ba 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -7,4 +7,9 @@ namespace :dev do Rake::Task["gitlab:setup"].invoke Rake::Task["gitlab:shell:setup"].invoke end + + desc 'GITLAB | Start/restart foreman and watch for changes' + task :foreman => :environment do + sh 'rerun --dir app,config,lib -- foreman start' + end end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 0230fbb010b..84445b3bf2f 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -27,9 +27,9 @@ namespace :gitlab do backup = Backup::Manager.new backup.unpack - Rake::Task["gitlab:backup:db:restore"].invoke - Rake::Task["gitlab:backup:repo:restore"].invoke - Rake::Task["gitlab:backup:uploads:restore"].invoke + Rake::Task["gitlab:backup:db:restore"].invoke unless backup.skipped?("db") + Rake::Task["gitlab:backup:repo:restore"].invoke unless backup.skipped?("repositories") + Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads") Rake::Task["gitlab:shell:setup"].invoke backup.cleanup @@ -38,8 +38,13 @@ namespace :gitlab do namespace :repo do task create: :environment do $progress.puts "Dumping repositories ...".blue - Backup::Repository.new.dump - $progress.puts "done".green + + if ENV["SKIP"] && ENV["SKIP"].include?("repositories") + $progress.puts "[SKIPPED]".cyan + else + Backup::Repository.new.dump + $progress.puts "done".green + end end task restore: :environment do @@ -52,8 +57,13 @@ namespace :gitlab do namespace :db do task create: :environment do $progress.puts "Dumping database ... ".blue - Backup::Database.new.dump - $progress.puts "done".green + + if ENV["SKIP"] && ENV["SKIP"].include?("db") + $progress.puts "[SKIPPED]".cyan + else + Backup::Database.new.dump + $progress.puts "done".green + end end task restore: :environment do @@ -66,8 +76,13 @@ namespace :gitlab do namespace :uploads do task create: :environment do $progress.puts "Dumping uploads ... ".blue - Backup::Uploads.new.dump - $progress.puts "done".green + + if ENV["SKIP"] && ENV["SKIP"].include?("uploads") + $progress.puts "[SKIPPED]".cyan + else + Backup::Uploads.new.dump + $progress.puts "done".green + end end task restore: :environment do diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 7ff23a7600a..75bd41f2838 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -1,7 +1,6 @@ namespace :gitlab do desc "GITLAB | Check the configuration of GitLab and its environment" - task check: %w{gitlab:env:check - gitlab:gitlab_shell:check + task check: %w{gitlab:gitlab_shell:check gitlab:sidekiq:check gitlab:ldap:check gitlab:app:check} @@ -14,6 +13,7 @@ namespace :gitlab do warn_user_is_not_gitlab start_checking "GitLab" + check_git_config check_database_config_exists check_database_is_not_sqlite check_migrations_are_up @@ -29,6 +29,7 @@ namespace :gitlab do check_redis_version check_ruby_version check_git_version + check_active_users finished_checking "GitLab" end @@ -37,6 +38,36 @@ namespace :gitlab do # Checks ######################## + def check_git_config + print "Git configured with autocrlf=input? ... " + + options = { + "core.autocrlf" => "input" + } + + correct_options = options.map do |name, value| + run(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value + end + + if correct_options.all? + puts "yes".green + else + print "Trying to fix Git error automatically. ..." + + if auto_fix_git_config(options) + puts "Success".green + else + puts "Failed".red + try_fixing_it( + sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") + ) + for_more_information( + see_installation_guide_section "GitLab" + ) + end + end + end + def check_database_config_exists print "Database config exists? ... " @@ -281,7 +312,8 @@ namespace :gitlab do def check_redis_version print "Redis version >= 2.0.0? ... " - if run_and_match(%W(redis-cli --version), /redis-cli 2.\d.\d/) + redis_version = run(%W(redis-cli --version)) + if redis_version.try(:match, /redis-cli 2.\d.\d/) || redis_version.try(:match, /redis-cli 3.\d.\d/) puts "yes".green else puts "no".red @@ -296,54 +328,6 @@ namespace :gitlab do end end - - - namespace :env do - desc "GITLAB | Check the configuration of the environment" - task check: :environment do - warn_user_is_not_gitlab - start_checking "Environment" - - check_gitlab_git_config - - finished_checking "Environment" - end - - - # Checks - ######################## - - def check_gitlab_git_config - print "Git configured for #{gitlab_user} user? ... " - - options = { - "user.name" => "GitLab", - "user.email" => Gitlab.config.gitlab.email_from, - "core.autocrlf" => "input" - } - correct_options = options.map do |name, value| - run(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value - end - - if correct_options.all? - puts "yes".green - else - puts "no".red - try_fixing_it( - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.name \"#{options["user.name"]}\""), - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.email \"#{options["user.email"]}\""), - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") - ) - for_more_information( - see_installation_guide_section "GitLab" - ) - fix_and_rerun - end - end - end - - - namespace :gitlab_shell do desc "GITLAB | Check the configuration of GitLab Shell" task check: :environment do @@ -585,10 +569,6 @@ namespace :gitlab do def gitlab_shell_patch_version Gitlab::Shell.version_required.split('.')[2].to_i end - - def has_gitlab_shell3? - gitlab_shell_version.try(:start_with?, "v3.") - end end @@ -686,6 +666,23 @@ namespace :gitlab do end end + namespace :repo do + desc "GITLAB | Check the integrity of the repositories managed by GitLab" + task check: :environment do + namespace_dirs = Dir.glob( + File.join(Gitlab.config.gitlab_shell.repos_path, '*') + ) + + namespace_dirs.each do |namespace_dir| + repo_dirs = Dir.glob(File.join(namespace_dir, '*')) + repo_dirs.each do |dir| + puts "\nChecking repo at #{dir}" + system(*%w(git fsck), chdir: dir) + end + end + end + end + # Helper methods ########################## @@ -785,19 +782,23 @@ namespace :gitlab do end end + def check_active_users + puts "Active users: #{User.active.count}" + end + def omnibus_gitlab? Dir.pwd == '/opt/gitlab/embedded/service/gitlab-rails' end def sanitized_message(project) - if sanitize + if should_sanitize? "#{project.namespace_id.to_s.yellow}/#{project.id.to_s.yellow} ... " else "#{project.name_with_namespace.yellow} ... " end end - def sanitize + def should_sanitize? if ENV['SANITIZE'] == "true" true else @@ -805,3 +806,4 @@ namespace :gitlab do end end end + diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 189ad6090a4..d49cb6778f1 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -51,7 +51,7 @@ namespace :gitlab do git_base_path = Gitlab.config.gitlab_shell.repos_path all_dirs = Dir.glob(git_base_path + '/*') - global_projects = Project.where(namespace_id: nil).pluck(:path) + global_projects = Project.in_namespace(nil).pluck(:path) puts git_base_path.yellow puts "Looking for global repos to remove... " @@ -90,13 +90,14 @@ namespace :gitlab do warn_user_is_not_gitlab block_flag = ENV['BLOCK'] - User.ldap.each do |ldap_user| - print "#{ldap_user.name} (#{ldap_user.extern_uid}) ..." - if Gitlab::LDAP::Access.allowed?(ldap_user) + User.find_each do |user| + next unless user.ldap_user? + print "#{user.name} (#{user.ldap_identity.extern_uid}) ..." + if Gitlab::LDAP::Access.allowed?(user) puts " [OK]".green else if block_flag - ldap_user.block! unless ldap_user.blocked? + user.block! unless user.blocked? puts " [BLOCKED]".red else puts " [NOT IN LDAP]".yellow diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index 3c693546c09..7c98ad3144f 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -25,7 +25,7 @@ namespace :gitlab do puts "Processing #{repo_path}".yellow - if path =~ /\.wiki\Z/ + if path.end_with?('.wiki') puts " * Skipping wiki repo" next end @@ -35,7 +35,7 @@ namespace :gitlab do if project puts " * #{project.name} (#{repo_path}) exists" else - user = User.admins.first + user = User.admins.reorder("id").first project_params = { name: name, @@ -66,6 +66,7 @@ namespace :gitlab do puts " * Created #{project.name} (#{repo_path})".green else puts " * Failed trying to create #{project.name} (#{repo_path})".red + puts " Validation Errors: #{project.errors.messages}".red end end end diff --git a/lib/tasks/gitlab/mail_google_schema_whitelisting.rake b/lib/tasks/gitlab/mail_google_schema_whitelisting.rake new file mode 100644 index 00000000000..102c6ae55d5 --- /dev/null +++ b/lib/tasks/gitlab/mail_google_schema_whitelisting.rake @@ -0,0 +1,73 @@ +require "#{Rails.root}/app/helpers/emails_helper" +require 'action_view/helpers' +extend ActionView::Helpers + +include ActionView::Context +include EmailsHelper + +namespace :gitlab do + desc "Email google whitelisting email with example email for actions in inbox" + task mail_google_schema_whitelisting: :environment do + subject = "Rails | Implemented feature" + url = "#{Gitlab.config.gitlab.url}/base/rails-project/issues/#{rand(1..100)}#note_#{rand(10..1000)}" + schema = email_action(url) + body = email_template(schema, url) + mail = Notify.test_email("schema.whitelisting+sample@gmail.com", subject, body.html_safe) + if send_now + mail.deliver + else + puts "WOULD SEND:" + end + puts mail + end + + def email_template(schema, url) + "<html lang='en'> + <head> + <meta content='text/html; charset=utf-8' http-equiv='Content-Type'> + <title> + GitLab + </title> + </meta> + </head> + <style> + img { + max-width: 100%; + height: auto; + } + p.details { + font-style:italic; + color:#777 + } + .footer p { + font-size:small; + color:#777 + } + </style> + <body> + <div class='content'> + <div> + <p>I like it :+1: </p> + </div> + </div> + + <div class='footer' style='margin-top: 10px;'> + <p> + <br> + <a href=\"#{url}\">View it on GitLab</a> + You're receiving this notification because you are a member of the Base / Rails Project project team. + #{schema} + </p> + </div> + </body> + </html>" + end + + def send_now + if ENV['SEND'] == "true" + true + else + false + end + end +end diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 202e55c89ad..afdaba11cb0 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -17,15 +17,19 @@ namespace :gitlab do # Clone if needed unless File.directory?(target_dir) - sh(*%W(git clone #{args.repo} #{target_dir})) + system(*%W(git clone -- #{args.repo} #{target_dir})) end # Make sure we're on the right tag Dir.chdir(target_dir) do # First try to checkout without fetching # to avoid stalling tests if the Internet is down. - reset = "git reset --hard $(git describe #{args.tag} || git describe origin/#{args.tag})" - sh "#{reset} || git fetch origin && #{reset}" + reseted = reset_to_commit(args) + + unless reseted + system(*%W(git fetch origin)) + reset_to_commit(args) + end config = { user: user, @@ -54,7 +58,10 @@ namespace :gitlab do File.open("config.yml", "w+") {|f| f.puts config.to_yaml} # Launch installation process - sh "bin/install" + system(*%W(bin/install)) + + # (Re)create hooks + system(*%W(bin/create-hooks)) end # Required for debian packaging with PKGR: Setup .ssh/environment with @@ -108,6 +115,7 @@ namespace :gitlab do print '.' end end + puts "" unless $?.success? puts "Failed to add keys...".red @@ -118,5 +126,16 @@ namespace :gitlab do puts "Quitting...".red exit 1 end + + def reset_to_commit(args) + tag, status = Gitlab::Popen.popen(%W(git describe -- #{args.tag})) + + unless status.zero? + tag, status = Gitlab::Popen.popen(%W(git describe -- origin/#{args.tag})) + end + + tag = tag.strip + system(*%W(git reset --hard #{tag})) + end end diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index da61c6e007f..c95b6540ebc 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -112,4 +112,20 @@ namespace :gitlab do @warned_user_not_gitlab = true end end + + # Tries to configure git itself + # + # Returns true if all subcommands were successfull (according to their exit code) + # Returns false if any or all subcommands failed. + def auto_fix_git_config(options) + if !@warned_user_not_gitlab + command_success = options.map do |name, value| + system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value})) + end + + command_success.all? + else + false + end + end end diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index c01b00bd1c0..b4c0ae3ff79 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -2,6 +2,8 @@ namespace :gitlab do desc "GITLAB | Run all tests" task :test do cmds = [ + %W(rake brakeman), + %W(rake rubocop), %W(rake spinach), %W(rake spec), %W(rake jasmine:ci) diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake index f9f586db93c..412bcad1229 100644 --- a/lib/tasks/gitlab/web_hook.rake +++ b/lib/tasks/gitlab/web_hook.rake @@ -51,11 +51,11 @@ namespace :gitlab do if namespace_path.blank? Project elsif namespace_path == '/' - Project.where(namespace_id: nil) + Project.in_namespace(nil) else namespace = Namespace.where(path: namespace_path).first if namespace - Project.where(namespace_id: namespace.id) + Project.in_namespace(namespace.id) else puts "Namespace not found: #{namespace_path}".red exit 2 diff --git a/lib/tasks/jasmine.rake b/lib/tasks/jasmine.rake new file mode 100644 index 00000000000..ac307a9e929 --- /dev/null +++ b/lib/tasks/jasmine.rake @@ -0,0 +1,12 @@ +# Since we no longer explicitly require the 'jasmine' gem, we lost the +# `jasmine:ci` task used by GitLab CI jobs. +# +# This provides a simple alias to run the `spec:javascript` task from the +# 'jasmine-rails' gem. +task jasmine: ['jasmine:ci'] + +namespace :jasmine do + task :ci do + Rake::Task['teaspoon'].invoke + end +end diff --git a/lib/tasks/rubocop.rake b/lib/tasks/rubocop.rake new file mode 100644 index 00000000000..ddfaf5d51f2 --- /dev/null +++ b/lib/tasks/rubocop.rake @@ -0,0 +1,4 @@ +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 507b315759d..4aefc18ce14 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -2,9 +2,15 @@ Rake::Task["spinach"].clear if Rake::Task.task_defined?('spinach') desc "GITLAB | Run spinach" task :spinach do + tags = if ENV['SEMAPHORE'] + '~@tricky' + else + '~@semaphore' + end + cmds = [ %W(rake gitlab:setup), - %W(spinach), + %W(spinach --tags #{tags}), ] run_commands(cmds) end diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake index 583f4a876da..a39d9649876 100644 --- a/lib/tasks/test.rake +++ b/lib/tasks/test.rake @@ -9,5 +9,5 @@ unless Rails.env.production? require 'coveralls/rake/task' Coveralls::RakeTask.new desc "GITLAB | Run all tests on CI with simplecov" - task :test_ci => [:spinach, :spec, 'coveralls:push'] + task :test_ci => [:rubocop, :brakeman, 'jasmine:ci', :spinach, :spec, 'coveralls:push'] end diff --git a/lib/version_check.rb b/lib/version_check.rb new file mode 100644 index 00000000000..ea23344948c --- /dev/null +++ b/lib/version_check.rb @@ -0,0 +1,18 @@ +require "base64" + +# This class is used to build image URL to +# check if it is a new version for update +class VersionCheck + def data + { version: Gitlab::VERSION } + end + + def url + encoded_data = Base64.urlsafe_encode64(data.to_json) + "#{host}?gitlab_info=#{encoded_data}" + end + + def host + 'https://version.gitlab.com/check.png' + end +end diff --git a/public/404.html b/public/404.html index 867f193a98f..a0106bc760d 100644 --- a/public/404.html +++ b/public/404.html @@ -1,14 +1,15 @@ <!DOCTYPE html> <html> <head> - <title>The page you were looking for doesn't exist (404)</title> + <title>The page you're looking for could not be found (404)</title> <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> </head> <body> <h1>404</h1> - <h3>The page you were looking for doesn't exist.</h3> + <h3>The page you're looking for could not be found.</h3> <hr/> - <p>You may have mistyped the address or the page may have moved.</p> + <p>Make sure the address is correct and that the page hasn't moved.</p> + <p>Please contact your GitLab administrator if you think this is a mistake.</p> </body> </html> diff --git a/public/422.html b/public/422.html index b6c37ac5386..026997b48e3 100644 --- a/public/422.html +++ b/public/422.html @@ -1,16 +1,16 @@ <!DOCTYPE html> <html> <head> - <title>The change you wanted was rejected (422)</title> + <title>The change you requested was rejected (422)</title> <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> </head> <body> <!-- This file lives in public/422.html --> <h1>422</h1> - <div> - <h2>The change you wanted was rejected.</h2> - <p>Maybe you tried to change something you didn't have access to.</p> - </div> + <h3>The change you requested was rejected.</h3> + <hr /> + <p>Make sure you have access to the thing you tried to change.</p> + <p>Please contact your GitLab administrator if you think this is a mistake.</p> </body> </html> diff --git a/public/500.html b/public/500.html index c84b9e90e4b..08c11bbd05a 100644 --- a/public/500.html +++ b/public/500.html @@ -1,13 +1,14 @@ <!DOCTYPE html> <html> <head> - <title>We're sorry, but something went wrong (500)</title> + <title>Something went wrong (500)</title> <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> </head> <body> <h1>500</h1> - <h3>We're sorry, but something went wrong.</h3> + <h3>Whoops, something went wrong on our end.</h3> <hr/> + <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> </body> </html> diff --git a/public/502.html b/public/502.html index d171eccc927..9480a928439 100644 --- a/public/502.html +++ b/public/502.html @@ -6,8 +6,9 @@ </head> <body> <h1>502</h1> - <h3>GitLab is not responding.</h3> + <h3>Whoops, GitLab is taking too much time to respond.</h3> <hr/> + <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> </body> </html> diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png Binary files differindex 6f2e0dd090f..7da5f23ed9b 100644 --- a/public/apple-touch-icon-precomposed.png +++ b/public/apple-touch-icon-precomposed.png diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png Binary files differindex 6f2e0dd090f..7da5f23ed9b 100644 --- a/public/apple-touch-icon.png +++ b/public/apple-touch-icon.png diff --git a/public/deploy.html b/public/deploy.html index d9c4bb5c583..3822ed4b64d 100644 --- a/public/deploy.html +++ b/public/deploy.html @@ -1,11 +1,17 @@ <!DOCTYPE html> <html> <head> - <title>Deploy in progress. Please try again in few minutes</title> + <title>Deploy in progress</title> <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> </head> + <body> - <h1><center><img src="/gitlab_logo.png"/></center>Deploy in progress</h1> - <h3>Please try again in few minutes or contact your administrator.</h3> + <h1> + <img src="/logo.svg" /><br /> + Deploy in progress + </h1> + <h3>Please try again in a few minutes.</h3> + <hr/> + <p>Please contact your GitLab administrator if this problem persists.</p> </body> </html> diff --git a/public/favicon.ico b/public/favicon.ico Binary files differindex bfb74960c48..3479cbbb46f 100644 --- a/public/favicon.ico +++ b/public/favicon.ico diff --git a/public/gitlab_logo.png b/public/gitlab_logo.png Binary files differdeleted file mode 100644 index dbe6dabb784..00000000000 --- a/public/gitlab_logo.png +++ /dev/null diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 00000000000..c09785cb96f --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="210px" height="210px" viewBox="0 0 210 210" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"> + <!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch --> + <title>Slice 1</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"> + <g id="logo" sketch:type="MSLayerGroup" transform="translate(0.000000, 10.000000)"> + <g id="Page-1" sketch:type="MSShapeGroup"> + <g id="Fill-1-+-Group-24"> + <g id="Group-24"> + <g id="Group"> + <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329"></path> + <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26"></path> + <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326"></path> + <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329"></path> + <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26"></path> + <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326"></path> + <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329"></path> + </g> + </g> + </g> + </g> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/public/static.css b/public/static.css index c6f92ac01d9..0a2b6060d48 100644 --- a/public/static.css +++ b/public/static.css @@ -2,18 +2,24 @@ body { color: #666; text-align: center; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin:0; + margin: 0; width: 800px; margin: auto; font-size: 14px; } + h1 { font-size: 56px; line-height: 100px; font-weight: normal; color: #456; } -h2 { font-size: 24px; color: #666; line-height: 1.5em; } + +h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; +} h3 { color: #456; diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh new file mode 100755 index 00000000000..5525ab77435 --- /dev/null +++ b/scripts/prepare_build.sh @@ -0,0 +1,24 @@ +#!/bin/bash +if [ -f /.dockerinit ]; then + wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_1.9.0-1+b1_amd64.deb + dpkg -i phantomjs_1.9.0-1+b1_amd64.deb + + apt-get update -qq + apt-get install -y -qq libicu-dev libkrb5-dev cmake nodejs + + cp config/database.yml.mysql config/database.yml + sed -i 's/username:.*/username: root/g' config/database.yml + sed -i 's/password:.*/password:/g' config/database.yml + sed -i 's/# socket:.*/host: mysql/g' config/database.yml + + cp config/resque.yml.example config/resque.yml + sed -i 's/localhost/redis/g' config/resque.yml + FLAGS=(--deployment --path /cache) + export FLAGS +else + export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin + cp config/database.yml.mysql config/database.yml + sed "s/username\:.*$/username\: runner/" -i config/database.yml + sed "s/password\:.*$/password\: 'password'/" -i config/database.yml + sed "s/gitlabhq_test/gitlabhq_test_$((RANDOM/5000))/" -i config/database.yml +fi diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index cc32805f5ec..55851befc8c 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -7,27 +7,67 @@ describe ApplicationController do it 'should redirect if the user is over their password expiry' do user.password_expires_at = Time.new(2002) - user.ldap_user?.should be_false - controller.stub(:current_user).and_return(user) - controller.should_receive(:redirect_to) - controller.should_receive(:new_profile_password_path) + expect(user.ldap_user?).to be_falsey + allow(controller).to receive(:current_user).and_return(user) + expect(controller).to receive(:redirect_to) + expect(controller).to receive(:new_profile_password_path) controller.send(:check_password_expiration) end it 'should not redirect if the user is under their password expiry' do user.password_expires_at = Time.now + 20010101 - user.ldap_user?.should be_false - controller.stub(:current_user).and_return(user) - controller.should_not_receive(:redirect_to) + expect(user.ldap_user?).to be_falsey + allow(controller).to receive(:current_user).and_return(user) + expect(controller).not_to receive(:redirect_to) controller.send(:check_password_expiration) end it 'should not redirect if the user is over their password expiry but they are an ldap user' do user.password_expires_at = Time.new(2002) - user.stub(:ldap_user?).and_return(true) - controller.stub(:current_user).and_return(user) - controller.should_not_receive(:redirect_to) + allow(user).to receive(:ldap_user?).and_return(true) + allow(controller).to receive(:current_user).and_return(user) + expect(controller).not_to receive(:redirect_to) controller.send(:check_password_expiration) end end + + describe 'check labels authorization' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:controller) { ApplicationController.new } + + before do + project.team << [user, :guest] + allow(controller).to receive(:current_user).and_return(user) + allow(controller).to receive(:project).and_return(project) + end + + it 'should succeed if issues and MRs are enabled' do + project.issues_enabled = true + project.merge_requests_enabled = true + controller.send(:authorize_read_label!) + expect(response.status).to eq(200) + end + + it 'should succeed if issues are enabled, MRs are disabled' do + project.issues_enabled = true + project.merge_requests_enabled = false + controller.send(:authorize_read_label!) + expect(response.status).to eq(200) + end + + it 'should succeed if issues are disabled, MRs are enabled' do + project.issues_enabled = false + project.merge_requests_enabled = true + controller.send(:authorize_read_label!) + expect(response.status).to eq(200) + end + + it 'should fail if issues and MRs are disabled' do + project.issues_enabled = false + project.merge_requests_enabled = false + expect(controller).to receive(:access_denied!) + controller.send(:authorize_read_label!) + end + end end diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb new file mode 100644 index 00000000000..9ad9cb41cc1 --- /dev/null +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe AutocompleteController do + let!(:project) { create(:project) } + let!(:user) { create(:user) } + let!(:user2) { create(:user) } + + context 'project members' do + before do + sign_in(user) + project.team << [user, :master] + + get(:users, project_id: project.id) + end + + let(:body) { JSON.parse(response.body) } + + it { expect(body).to be_kind_of(Array) } + it { expect(body.size).to eq 1 } + it { expect(body.first["username"]).to eq user.username } + end + + context 'group members' do + let(:group) { create(:group) } + + before do + sign_in(user) + group.add_owner(user) + + get(:users, group_id: group.id) + end + + let(:body) { JSON.parse(response.body) } + + it { expect(body).to be_kind_of(Array) } + it { expect(body.size).to eq 1 } + it { expect(body.first["username"]).to eq user.username } + end + + context 'all users' do + before do + sign_in(user) + get(:users) + end + + let(:body) { JSON.parse(response.body) } + + it { expect(body).to be_kind_of(Array) } + it { expect(body.size).to eq User.count } + end +end diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb index 11d748ca77f..a1102f28340 100644 --- a/spec/controllers/blob_controller_spec.rb +++ b/spec/controllers/blob_controller_spec.rb @@ -9,29 +9,32 @@ describe Projects::BlobController do project.team << [user, :master] - project.stub(:branches).and_return(['master', 'foo/bar/baz']) - project.stub(:tags).and_return(['v1.0.0', 'v2.0.0']) + allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz']) + allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0']) controller.instance_variable_set(:@project, project) end describe "GET show" do render_views - before { get :show, project_id: project.to_param, id: id } + before do + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: id) + end context "valid branch, valid file" do let(:id) { 'master/README.md' } - it { should respond_with(:success) } + it { is_expected.to respond_with(:success) } end context "valid branch, invalid file" do let(:id) { 'master/invalid-path.rb' } - it { should respond_with(:not_found) } + it { is_expected.to respond_with(:not_found) } end context "invalid branch, valid file" do let(:id) { 'invalid-branch/README.md' } - it { should respond_with(:not_found) } + it { is_expected.to respond_with(:not_found) } end end @@ -39,13 +42,17 @@ describe Projects::BlobController do render_views before do - get :show, project_id: project.to_param, id: id + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: id) controller.instance_variable_set(:@blob, nil) end context 'redirect to tree' do let(:id) { 'markdown/doc' } - it { should redirect_to("/#{project.path_with_namespace}/tree/markdown/doc") } + it 'redirects' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/tree/markdown/doc") + end end end end diff --git a/spec/controllers/branches_controller_spec.rb b/spec/controllers/branches_controller_spec.rb index 610d7a84e31..51397382cfb 100644 --- a/spec/controllers/branches_controller_spec.rb +++ b/spec/controllers/branches_controller_spec.rb @@ -9,8 +9,8 @@ describe Projects::BranchesController do project.team << [user, :master] - project.stub(:branches).and_return(['master', 'foo/bar/baz']) - project.stub(:tags).and_return(['v1.0.0', 'v2.0.0']) + allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz']) + allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0']) controller.instance_variable_set(:@project, project) end @@ -19,6 +19,7 @@ describe Projects::BranchesController do before { post :create, + namespace_id: project.namespace.to_param, project_id: project.to_param, branch_name: branch, ref: ref @@ -27,25 +28,31 @@ describe Projects::BranchesController do context "valid branch name, valid source" do let(:branch) { "merge_branch" } let(:ref) { "master" } - it { should redirect_to("/#{project.path_with_namespace}/tree/merge_branch") } + it 'redirects' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/tree/merge_branch") + end end context "invalid branch name, valid ref" do let(:branch) { "<script>alert('merge');</script>" } let(:ref) { "master" } - it { should redirect_to("/#{project.path_with_namespace}/tree/alert('merge');") } + it 'redirects' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');") + end end context "valid branch name, invalid ref" do let(:branch) { "merge_branch" } let(:ref) { "<script>alert('ref');</script>" } - it { should render_template("new") } + it { is_expected.to render_template('new') } end context "invalid branch name, invalid ref" do let(:branch) { "<script>alert('merge');</script>" } let(:ref) { "<script>alert('ref');</script>" } - it { should render_template("new") } + it { is_expected.to render_template('new') } end end end diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb index f5822157ea4..34ee61f7ede 100644 --- a/spec/controllers/commit_controller_spec.rb +++ b/spec/controllers/commit_controller_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Projects::CommitController do let(:project) { create(:project) } let(:user) { create(:user) } - let(:commit) { project.repository.commit("master") } + let(:commit) { project.commit("master") } before do sign_in(user) @@ -13,32 +13,37 @@ describe Projects::CommitController do describe "#show" do shared_examples "export as" do |format| it "should generally work" do - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) expect(response).to be_success end it "should generate it" do - Commit.any_instance.should_receive(:"to_#{format}") + expect_any_instance_of(Commit).to receive(:"to_#{format}") - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) end it "should render it" do - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) expect(response.body).to eq(commit.send(:"to_#{format}")) end it "should not escape Html" do - Commit.any_instance.stub(:"to_#{format}").and_return('HTML entities &<>" ') + allow_any_instance_of(Commit).to receive(:"to_#{format}"). + and_return('HTML entities &<>" ') - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) - expect(response.body).to_not include('&') - expect(response.body).to_not include('>') - expect(response.body).to_not include('<') - expect(response.body).to_not include('"') + expect(response.body).not_to include('&') + expect(response.body).not_to include('>') + expect(response.body).not_to include('<') + expect(response.body).not_to include('"') end end @@ -47,7 +52,8 @@ describe Projects::CommitController do let(:format) { :diff } it "should really only be a git diff" do - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) expect(response.body).to start_with("diff --git") end @@ -58,16 +64,28 @@ describe Projects::CommitController do let(:format) { :patch } it "should really be a git email patch" do - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) expect(response.body).to start_with("From #{commit.id}") end it "should contain a git diff" do - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) expect(response.body).to match(/^diff --git/) end end end + + describe "#branches" do + it "contains branch and tags information" do + get(:branches, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id) + + expect(assigns(:branches)).to include("master", "feature_conflict") + expect(assigns(:tags)).to include("v1.1.0") + end + end end diff --git a/spec/controllers/commits_controller_spec.rb b/spec/controllers/commits_controller_spec.rb index 0c19d755eb1..2184b35152e 100644 --- a/spec/controllers/commits_controller_spec.rb +++ b/spec/controllers/commits_controller_spec.rb @@ -12,9 +12,10 @@ describe Projects::CommitsController do describe "GET show" do context "as atom feed" do it "should render as atom" do - get :show, project_id: project.to_param, id: "master", format: "atom" - response.should be_success - response.content_type.should == 'application/atom+xml' + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: "master", format: "atom") + expect(response).to be_success + expect(response.content_type).to eq('application/atom+xml') end end end diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb new file mode 100644 index 00000000000..3dac134a731 --- /dev/null +++ b/spec/controllers/groups/avatars_controller_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Groups::AvatarsController do + let(:user) { create(:user) } + let(:group) { create(:group, owner: user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + + before do + sign_in(user) + end + + it 'destroy should remove avatar from DB' do + delete :destroy, group_id: group.path + @group = assigns(:group) + expect(@group.avatar.present?).to be_falsey + expect(@group).to be_valid + end +end diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb new file mode 100644 index 00000000000..93535ced7ae --- /dev/null +++ b/spec/controllers/help_controller_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe HelpController do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe 'GET #show' do + context 'for Markdown formats' do + context 'when requested file exists' do + before do + get :show, category: 'ssh', file: 'README', format: :md + end + + it 'assigns to @markdown' do + expect(assigns[:markdown]).not_to be_empty + end + + it 'renders HTML' do + expect(response).to render_template('show.html.haml') + expect(response.content_type).to eq 'text/html' + end + end + + context 'when requested file is missing' do + it 'renders not found' do + get :show, category: 'foo', file: 'bar', format: :md + expect(response).to be_not_found + end + end + end + + context 'for image formats' do + context 'when requested file exists' do + it 'renders the raw file' do + get :show, category: 'workflow/protected_branches', + file: 'protected_branches1', format: :png + expect(response).to be_success + expect(response.content_type).to eq 'image/png' + expect(response.headers['Content-Disposition']).to match(/^inline;/) + end + end + + context 'when requested file is missing' do + it 'renders not found' do + get :show, category: 'foo', file: 'bar', format: :png + expect(response).to be_not_found + end + end + end + + context 'for other formats' do + it 'always renders not found' do + get :show, category: 'ssh', file: 'README', format: :foo + expect(response).to be_not_found + end + end + end +end diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb new file mode 100644 index 00000000000..f577c2b3006 --- /dev/null +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -0,0 +1,166 @@ +require 'spec_helper' +require_relative 'import_spec_helper' + +describe Import::BitbucketController do + include ImportSpecHelper + + let(:user) { create(:user, bitbucket_access_token: 'asd123', bitbucket_access_token_secret: "sekret") } + + before do + sign_in(user) + allow(controller).to receive(:bitbucket_import_enabled?).and_return(true) + end + + describe "GET callback" do + before do + session[:oauth_request_token] = {} + end + + it "updates access token" do + token = "asdasd12345" + secret = "sekrettt" + access_token = double(token: token, secret: secret) + allow_any_instance_of(Gitlab::BitbucketImport::Client). + to receive(:get_token).and_return(access_token) + stub_omniauth_provider('bitbucket') + + get :callback + + expect(user.reload.bitbucket_access_token).to eq(token) + expect(user.reload.bitbucket_access_token_secret).to eq(secret) + expect(controller).to redirect_to(status_import_bitbucket_url) + end + end + + describe "GET status" do + before do + @repo = OpenStruct.new(slug: 'vim', owner: 'asd') + end + + it "assigns variables" do + @project = create(:project, import_type: 'bitbucket', creator_id: user.id) + stub_client(projects: [@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([@repo]) + end + + it "does not show already added project" do + @project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim') + stub_client(projects: [@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([]) + end + end + + describe "POST create" do + let(:bitbucket_username) { user.username } + + let(:bitbucket_user) { + { + user: { + username: bitbucket_username + } + }.with_indifferent_access + } + + let(:bitbucket_repo) { + { + slug: "vim", + owner: bitbucket_username + }.with_indifferent_access + } + + before do + allow(Gitlab::BitbucketImport::KeyAdder). + to receive(:new).with(bitbucket_repo, user). + and_return(double(execute: true)) + + stub_client(user: bitbucket_user, project: bitbucket_repo) + end + + context "when the repository owner is the Bitbucket user" do + context "when the Bitbucket user and GitLab user's usernames match" do + it "takes the current user's namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(bitbucket_repo, user.namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the Bitbucket user and GitLab user's usernames don't match" do + let(:bitbucket_username) { "someone_else" } + + it "takes the current user's namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(bitbucket_repo, user.namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context "when the repository owner is not the Bitbucket user" do + let(:other_username) { "someone_else" } + + before do + bitbucket_repo["owner"] = other_username + end + + context "when a namespace with the Bitbucket user's username already exists" do + let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + + context "when the namespace is owned by the GitLab user" do + it "takes the existing namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(bitbucket_repo, existing_namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the namespace is not owned by the GitLab user" do + before do + existing_namespace.owner = create(:user) + existing_namespace.save + end + + it "doesn't create a project" do + expect(Gitlab::BitbucketImport::ProjectCreator). + not_to receive(:new) + + post :create, format: :js + end + end + end + + context "when a namespace with the Bitbucket user's username doesn't exist" do + it "creates the namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + post :create, format: :js + + expect(Namespace.where(name: other_username).first).not_to be_nil + end + + it "takes the new namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(bitbucket_repo, an_instance_of(Group), user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + end +end diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb new file mode 100644 index 00000000000..9534981c78b --- /dev/null +++ b/spec/controllers/import/github_controller_spec.rb @@ -0,0 +1,150 @@ +require 'spec_helper' +require_relative 'import_spec_helper' + +describe Import::GithubController do + include ImportSpecHelper + + let(:user) { create(:user, github_access_token: 'asd123') } + + before do + sign_in(user) + allow(controller).to receive(:github_import_enabled?).and_return(true) + end + + describe "GET callback" do + it "updates access token" do + token = "asdasd12345" + allow_any_instance_of(Gitlab::GithubImport::Client). + to receive(:get_token).and_return(token) + stub_omniauth_provider('github') + + get :callback + + expect(user.reload.github_access_token).to eq(token) + expect(controller).to redirect_to(status_import_github_url) + end + end + + describe "GET status" do + before do + @repo = OpenStruct.new(login: 'vim', full_name: 'asd/vim') + @org = OpenStruct.new(login: 'company') + @org_repo = OpenStruct.new(login: 'company', full_name: 'company/repo') + end + + it "assigns variables" do + @project = create(:project, import_type: 'github', creator_id: user.id) + stub_client(repos: [@repo], orgs: [@org], org_repos: [@org_repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([@repo, @org_repo]) + end + + it "does not show already added project" do + @project = create(:project, import_type: 'github', creator_id: user.id, import_source: 'asd/vim') + stub_client(repos: [@repo], orgs: []) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([]) + end + end + + describe "POST create" do + let(:github_username) { user.username } + + let(:github_user) { + OpenStruct.new(login: github_username) + } + + let(:github_repo) { + OpenStruct.new(name: 'vim', full_name: "#{github_username}/vim", owner: OpenStruct.new(login: github_username)) + } + + before do + stub_client(user: github_user, repo: github_repo) + end + + context "when the repository owner is the GitHub user" do + context "when the GitHub user and GitLab user's usernames match" do + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(github_repo, user.namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the GitHub user and GitLab user's usernames don't match" do + let(:github_username) { "someone_else" } + + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(github_repo, user.namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context "when the repository owner is not the GitHub user" do + let(:other_username) { "someone_else" } + + before do + github_repo.owner = OpenStruct.new(login: other_username) + end + + context "when a namespace with the GitHub user's username already exists" do + let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + + context "when the namespace is owned by the GitLab user" do + it "takes the existing namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(github_repo, existing_namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the namespace is not owned by the GitLab user" do + before do + existing_namespace.owner = create(:user) + existing_namespace.save + end + + it "doesn't create a project" do + expect(Gitlab::GithubImport::ProjectCreator). + not_to receive(:new) + + post :create, format: :js + end + end + end + + context "when a namespace with the GitHub user's username doesn't exist" do + it "creates the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + post :create, format: :js + + expect(Namespace.where(name: other_username).first).not_to be_nil + end + + it "takes the new namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(github_repo, an_instance_of(Group), user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + end +end diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb new file mode 100644 index 00000000000..cb06cdc09ea --- /dev/null +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -0,0 +1,155 @@ +require 'spec_helper' +require_relative 'import_spec_helper' + +describe Import::GitlabController do + include ImportSpecHelper + + let(:user) { create(:user, gitlab_access_token: 'asd123') } + + before do + sign_in(user) + allow(controller).to receive(:gitlab_import_enabled?).and_return(true) + end + + describe "GET callback" do + it "updates access token" do + token = "asdasd12345" + allow_any_instance_of(Gitlab::GitlabImport::Client). + to receive(:get_token).and_return(token) + stub_omniauth_provider('gitlab') + + get :callback + + expect(user.reload.gitlab_access_token).to eq(token) + expect(controller).to redirect_to(status_import_gitlab_url) + end + end + + describe "GET status" do + before do + @repo = OpenStruct.new(path: 'vim', path_with_namespace: 'asd/vim') + end + + it "assigns variables" do + @project = create(:project, import_type: 'gitlab', creator_id: user.id) + stub_client(projects: [@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([@repo]) + end + + it "does not show already added project" do + @project = create(:project, import_type: 'gitlab', creator_id: user.id, import_source: 'asd/vim') + stub_client(projects: [@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([]) + end + end + + describe "POST create" do + let(:gitlab_username) { user.username } + + let(:gitlab_user) { + { + username: gitlab_username + }.with_indifferent_access + } + + let(:gitlab_repo) { + { + path: 'vim', + path_with_namespace: "#{gitlab_username}/vim", + owner: { name: gitlab_username }, + namespace: { path: gitlab_username } + }.with_indifferent_access + } + + before do + stub_client(user: gitlab_user, project: gitlab_repo) + end + + context "when the repository owner is the GitLab.com user" do + context "when the GitLab.com user and GitLab server user's usernames match" do + it "takes the current user's namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(gitlab_repo, user.namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the GitLab.com user and GitLab server user's usernames don't match" do + let(:gitlab_username) { "someone_else" } + + it "takes the current user's namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(gitlab_repo, user.namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context "when the repository owner is not the GitLab.com user" do + let(:other_username) { "someone_else" } + + before do + gitlab_repo["namespace"]["path"] = other_username + end + + context "when a namespace with the GitLab.com user's username already exists" do + let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + + context "when the namespace is owned by the GitLab server user" do + it "takes the existing namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(gitlab_repo, existing_namespace, user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the namespace is not owned by the GitLab server user" do + before do + existing_namespace.owner = create(:user) + existing_namespace.save + end + + it "doesn't create a project" do + expect(Gitlab::GitlabImport::ProjectCreator). + not_to receive(:new) + + post :create, format: :js + end + end + end + + context "when a namespace with the GitLab.com user's username doesn't exist" do + it "creates the namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + post :create, format: :js + + expect(Namespace.where(name: other_username).first).not_to be_nil + end + + it "takes the new namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(gitlab_repo, an_instance_of(Group), user). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + end +end diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb new file mode 100644 index 00000000000..7cb1b85a46d --- /dev/null +++ b/spec/controllers/import/gitorious_controller_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' +require_relative 'import_spec_helper' + +describe Import::GitoriousController do + include ImportSpecHelper + + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe "GET new" do + it "redirects to import endpoint on gitorious.org" do + get :new + + expect(controller).to redirect_to("https://gitorious.org/gitlab-import?callback_url=http://test.host/import/gitorious/callback") + end + end + + describe "GET callback" do + it "stores repo list in session" do + get :callback, repos: 'foo/bar,baz/qux' + + expect(session[:gitorious_repos]).to eq('foo/bar,baz/qux') + end + end + + describe "GET status" do + before do + @repo = OpenStruct.new(full_name: 'asd/vim') + end + + it "assigns variables" do + @project = create(:project, import_type: 'gitorious', creator_id: user.id) + stub_client(repos: [@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([@repo]) + end + + it "does not show already added project" do + @project = create(:project, import_type: 'gitorious', creator_id: user.id, import_source: 'asd/vim') + stub_client(repos: [@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([]) + end + end + + describe "POST create" do + before do + @repo = Gitlab::GitoriousImport::Repository.new('asd/vim') + end + + it "takes already existing namespace" do + namespace = create(:namespace, name: "asd", owner: user) + expect(Gitlab::GitoriousImport::ProjectCreator). + to receive(:new).with(@repo, namespace, user). + and_return(double(execute: true)) + stub_client(repo: @repo) + + post :create, format: :js + end + end +end diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb new file mode 100644 index 00000000000..66088139a69 --- /dev/null +++ b/spec/controllers/import/google_code_controller_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' +require_relative 'import_spec_helper' + +describe Import::GoogleCodeController do + include ImportSpecHelper + + let(:user) { create(:user) } + let(:dump_file) { fixture_file_upload(Rails.root + 'spec/fixtures/GoogleCodeProjectHosting.json', 'application/json') } + + before do + sign_in(user) + end + + describe "POST callback" do + it "stores Google Takeout dump list in session" do + post :callback, dump_file: dump_file + + expect(session[:google_code_dump]).to be_a(Hash) + expect(session[:google_code_dump]["kind"]).to eq("projecthosting#user") + expect(session[:google_code_dump]).to have_key("projects") + end + end + + describe "GET status" do + before do + @repo = OpenStruct.new(name: 'vim') + stub_client(valid?: true) + end + + it "assigns variables" do + @project = create(:project, import_type: 'google_code', creator_id: user.id) + stub_client(repos: [@repo], incompatible_repos: []) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([@repo]) + expect(assigns(:incompatible_repos)).to eq([]) + end + + it "does not show already added project" do + @project = create(:project, import_type: 'google_code', creator_id: user.id, import_source: 'vim') + stub_client(repos: [@repo], incompatible_repos: []) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([]) + end + + it "does not show any invalid projects" do + stub_client(repos: [], incompatible_repos: [@repo]) + + get :status + + expect(assigns(:repos)).to be_empty + expect(assigns(:incompatible_repos)).to eq([@repo]) + end + end +end diff --git a/spec/controllers/import/import_spec_helper.rb b/spec/controllers/import/import_spec_helper.rb new file mode 100644 index 00000000000..9d7648e25a7 --- /dev/null +++ b/spec/controllers/import/import_spec_helper.rb @@ -0,0 +1,33 @@ +require 'ostruct' + +# Helper methods for controller specs in the Import namespace +# +# Must be included manually. +module ImportSpecHelper + # Stub `controller` to return a null object double with the provided messages + # when `client` is called + # + # Examples: + # + # stub_client(foo: %w(foo)) + # + # controller.client.foo # => ["foo"] + # controller.client.bar.baz.foo # => ["foo"] + # + # Returns the client double + def stub_client(messages = {}) + client = double('client', messages).as_null_object + allow(controller).to receive(:client).and_return(client) + + client + end + + def stub_omniauth_provider(name) + provider = OpenStruct.new( + name: name, + app_id: 'asd123', + app_secret: 'asd123' + ) + Gitlab.config.omniauth.providers << provider + end +end diff --git a/spec/controllers/merge_requests_controller_spec.rb b/spec/controllers/merge_requests_controller_spec.rb deleted file mode 100644 index 300527e4ff2..00000000000 --- a/spec/controllers/merge_requests_controller_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'spec_helper' - -describe Projects::MergeRequestsController do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } - - before do - sign_in(user) - project.team << [user, :master] - end - - describe "#show" do - shared_examples "export merge as" do |format| - it "should generally work" do - get :show, project_id: project.to_param, id: merge_request.iid, format: format - - expect(response).to be_success - end - - it "should generate it" do - MergeRequest.any_instance.should_receive(:"to_#{format}") - - get :show, project_id: project.to_param, id: merge_request.iid, format: format - end - - it "should render it" do - get :show, project_id: project.to_param, id: merge_request.iid, format: format - - expect(response.body).to eq((merge_request.send(:"to_#{format}",user)).to_s) - end - - it "should not escape Html" do - MergeRequest.any_instance.stub(:"to_#{format}").and_return('HTML entities &<>" ') - - get :show, project_id: project.to_param, id: merge_request.iid, format: format - - expect(response.body).to_not include('&') - expect(response.body).to_not include('>') - expect(response.body).to_not include('<') - expect(response.body).to_not include('"') - end - end - - describe "as diff" do - include_examples "export merge as", :diff - let(:format) { :diff } - - it "should really only be a git diff" do - get :show, project_id: project.to_param, id: merge_request.iid, format: format - - expect(response.body).to start_with("diff --git") - end - end - - describe "as patch" do - include_examples "export merge as", :patch - let(:format) { :patch } - - it "should really be a git email patch with commit" do - get :show, project_id: project.to_param, id: merge_request.iid, format: format - - expect(response.body[0..100]).to start_with("From #{merge_request.commits.last.id}") - end - - it "should contain git diffs" do - get :show, project_id: project.to_param, id: merge_request.iid, format: format - - expect(response.body).to match(/^diff --git/) - end - end - end -end diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb new file mode 100644 index 00000000000..9c8619722cd --- /dev/null +++ b/spec/controllers/namespaces_controller_spec.rb @@ -0,0 +1,121 @@ +require 'spec_helper' + +describe NamespacesController do + let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + + describe "GET show" do + context "when the namespace belongs to a user" do + let!(:other_user) { create(:user) } + + it "redirects to the user's page" do + get :show, id: other_user.username + + expect(response).to redirect_to(user_path(other_user)) + end + end + + context "when the namespace belongs to a group" do + let!(:group) { create(:group) } + let!(:project) { create(:project, namespace: group) } + + context "when the group has public projects" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + context "when not signed in" do + it "redirects to the group's page" do + get :show, id: group.path + + expect(response).to redirect_to(group_path(group)) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + it "redirects to the group's page" do + get :show, id: group.path + + expect(response).to redirect_to(group_path(group)) + end + end + end + + context "when the project doesn't have public projects" do + context "when not signed in" do + it "redirects to the sign in page" do + get :show, id: group.path + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when signed in" do + before do + sign_in(user) + 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 "redirects to the sign in page" do + get :show, id: group.path + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "redirects to the group's page" do + get :show, id: group.path + + expect(response).to redirect_to(group_path(group)) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + get :show, id: group.path + + expect(response.status).to eq(404) + end + end + end + end + end + + context "when the namespace doesn't exist" do + context "when signed in" do + before do + sign_in(user) + end + + it "responds with status 404" do + get :show, id: "doesntexist" + + expect(response.status).to eq(404) + end + end + + context "when not signed in" do + it "redirects to the sign in page" do + get :show, id: "doesntexist" + + expect(response).to redirect_to(new_user_session_path) + end + end + end + end +end diff --git a/spec/controllers/profiles/avatars_controller_spec.rb b/spec/controllers/profiles/avatars_controller_spec.rb new file mode 100644 index 00000000000..ad5855df0a4 --- /dev/null +++ b/spec/controllers/profiles/avatars_controller_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Profiles::AvatarsController do + let(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png")) } + + before do + sign_in(user) + controller.instance_variable_set(:@user, user) + end + + it 'destroy should remove avatar from DB' do + delete :destroy + @user = assigns(:user) + expect(@user.avatar.present?).to be_falsey + expect(@user).to be_valid + end +end diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb new file mode 100644 index 00000000000..1f0943c93d8 --- /dev/null +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Profiles::PreferencesController do + let(:user) { create(:user) } + + before do + sign_in(user) + + allow(subject).to receive(:current_user).and_return(user) + end + + describe 'GET show' do + it 'renders' do + get :show + expect(response).to render_template :show + end + + it 'assigns user' do + get :show + expect(assigns[:user]).to eq user + end + end + + describe 'PATCH update' do + def go(params: {}, format: :js) + params.reverse_merge!( + color_scheme_id: '1', + dashboard: 'stars', + theme_id: '1' + ) + + patch :update, user: params, format: format + end + + context 'on successful update' do + it 'sets the flash' do + go + expect(flash[:notice]).to eq 'Preferences saved.' + end + + it "changes the user's preferences" do + prefs = { + color_scheme_id: '1', + dashboard: 'stars', + theme_id: '2' + }.with_indifferent_access + + expect(user).to receive(:update_attributes).with(prefs) + + go params: prefs + end + end + + context 'on failed update' do + it 'sets the flash' do + expect(user).to receive(:update_attributes).and_return(false) + + go + + expect(flash[:alert]).to eq('Failed to save preferences.') + end + end + + context 'on invalid dashboard setting' do + it 'sets the flash' do + prefs = {dashboard: 'invalid'} + + go params: prefs + + expect(flash[:alert]).to match(/\AFailed to save preferences \(.+\)\.\z/) + end + end + + context 'as js' do + it 'renders' do + go + expect(response).to render_template :update + end + end + + context 'as html' do + it 'redirects' do + go format: :html + expect(response).to redirect_to(profile_preferences_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 new file mode 100644 index 00000000000..aa09f1a758d --- /dev/null +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe Profiles::TwoFactorAuthsController do + before do + # `user` should be defined within the action-specific describe blocks + sign_in(user) + + allow(subject).to receive(:current_user).and_return(user) + end + + describe 'GET new' 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 + end + + it 'assigns qr_code' do + code = double('qr code') + expect(subject).to receive(:build_qr_code).and_return(code) + + get :new + expect(assigns[:qr_code]).to eq code + end + end + + describe 'POST create' do + let(:user) { create(:user) } + let(:pin) { 'pin-code' } + + def go + post :create, pin_code: pin + end + + context 'with valid pin' do + before do + expect(user).to receive(:valid_otp?).with(pin).and_return(true) + end + + it 'sets two_factor_enabled' do + go + + user.reload + expect(user).to be_two_factor_enabled + end + + it 'presents plaintext codes for the user to save' do + expect(user).to receive(:generate_otp_backup_codes!).and_return(%w(a b c)) + + go + + expect(assigns[:codes]).to match_array %w(a b c) + end + + it 'renders create' do + go + expect(response).to render_template(:create) + end + end + + context 'with invalid pin' do + before do + expect(user).to receive(:valid_otp?).with(pin).and_return(false) + end + + it 'assigns error' do + go + expect(assigns[:error]).to eq 'Invalid pin code' + end + + it 'assigns qr_code' do + code = double('qr code') + expect(subject).to receive(:build_qr_code).and_return(code) + + go + expect(assigns[:qr_code]).to eq code + end + + it 'renders new' do + go + expect(response).to render_template(:new) + end + end + end + + describe 'POST codes' do + let(:user) { create(:user, :two_factor) } + + it 'presents plaintext codes for the user to save' do + expect(user).to receive(:generate_otp_backup_codes!).and_return(%w(a b c)) + + post :codes + expect(assigns[:codes]).to match_array %w(a b c) + end + + it 'persists the generated codes' do + post :codes + + user.reload + expect(user.otp_backup_codes).not_to be_empty + end + end + + describe 'DELETE destroy' do + let(:user) { create(:user, :two_factor) } + let!(:codes) { user.generate_otp_backup_codes! } + + it 'clears all 2FA-related fields' do + expect(user).to be_two_factor_enabled + expect(user.otp_backup_codes).not_to be_nil + expect(user.encrypted_otp_secret).not_to be_nil + + delete :destroy + + expect(user).not_to be_two_factor_enabled + expect(user.otp_backup_codes).to be_nil + expect(user.encrypted_otp_secret).to be_nil + end + + it 'redirects to profile_account_path' do + delete :destroy + + expect(response).to redirect_to(profile_account_path) + end + end +end diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb new file mode 100644 index 00000000000..e79b46a3504 --- /dev/null +++ b/spec/controllers/projects/avatars_controller_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Projects::AvatarsController do + let(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let(:user) { create(:user) } + + before do + sign_in(user) + project.team << [user, :developer] + controller.instance_variable_set(:@project, project) + end + + it 'destroy should remove avatar from DB' do + delete :destroy, namespace_id: project.namespace.id, project_id: project.id + expect(project.avatar.present?).to be_falsey + expect(project).to be_valid + end +end diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb new file mode 100644 index 00000000000..23e1566b8f3 --- /dev/null +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Projects::CompareController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:ref_from) { "improve%2Fawesome" } + let(:ref_to) { "feature" } + + before do + sign_in(user) + project.team << [user, :master] + end + + it 'compare should show some diffs' do + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, from: ref_from, to: ref_to) + + expect(response).to be_success + expect(assigns(:diffs).length).to be >= 1 + expect(assigns(:commits).length).to be >= 1 + end +end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb new file mode 100644 index 00000000000..b9c6f6e472e --- /dev/null +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -0,0 +1,150 @@ +require 'spec_helper' + +describe Projects::MergeRequestsController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + + before do + sign_in(user) + project.team << [user, :master] + end + + describe "#show" do + shared_examples "export merge as" do |format| + it "should generally work" do + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) + + expect(response).to be_success + end + + it "should generate it" do + expect_any_instance_of(MergeRequest).to receive(:"to_#{format}") + + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) + end + + it "should render it" do + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) + + expect(response.body).to eq((merge_request.send(:"to_#{format}",user)).to_s) + end + + it "should not escape Html" do + allow_any_instance_of(MergeRequest).to receive(:"to_#{format}"). + and_return('HTML entities &<>" ') + + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) + + expect(response.body).not_to include('&') + expect(response.body).not_to include('>') + expect(response.body).not_to include('<') + expect(response.body).not_to include('"') + end + end + + describe "as diff" do + include_examples "export merge as", :diff + let(:format) { :diff } + + it "should really only be a git diff" do + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) + + expect(response.body).to start_with("diff --git") + end + end + + describe "as patch" do + include_examples "export merge as", :patch + let(:format) { :patch } + + it "should really be a git email patch with commit" do + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) + + expect(response.body[0..100]).to start_with("From #{merge_request.commits.last.id}") + end + + it "should contain git diffs" do + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) + + expect(response.body).to match(/^diff --git/) + end + end + end + + describe 'GET diffs' do + def go(format: 'html') + get :diffs, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format + end + + context 'as html' do + it 'renders the diff template' do + go + + expect(response).to render_template('diffs') + end + end + + context 'as json' do + it 'renders the diffs template to a string' do + go format: 'json' + + expect(response).to render_template('projects/merge_requests/show/_diffs') + expect(JSON.parse(response.body)).to have_key('html') + end + end + + context 'with forked projects with submodules' do + render_views + + let(:project) { create(:project) } + let(:fork_project) { create(:forked_project_with_submodules) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + + before do + fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + fork_project.save + merge_request.reload + end + + it 'renders' do + go format: 'json' + + expect(response).to be_success + expect(response.body).to have_content('Subproject commit') + end + end + end + + describe 'GET commits' do + def go(format: 'html') + get :commits, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format + end + + context 'as html' do + it 'renders the show template' do + go + + expect(response).to render_template('show') + end + end + + context 'as json' do + it 'renders the commits template to a string' do + go format: 'json' + + expect(response).to render_template('projects/merge_requests/show/_commits') + expect(JSON.parse(response.body)).to have_key('html') + end + end + end +end diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb new file mode 100644 index 00000000000..596d8d34b7c --- /dev/null +++ b/spec/controllers/projects/protected_branches_controller_spec.rb @@ -0,0 +1,10 @@ +require('spec_helper') + +describe Projects::ProtectedBranchesController do + describe "GET #index" do + let(:project) { create(:project_empty_repo, :public) } + it "redirect empty repo to projects page" do + get(:index, namespace_id: project.namespace.to_param, project_id: project.to_param) + end + end +end diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb new file mode 100644 index 00000000000..c254ab7cb6e --- /dev/null +++ b/spec/controllers/projects/refs_controller_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Projects::RefsController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + sign_in(user) + project.team << [user, :developer] + end + + describe 'GET #logs_tree' do + def default_get(format = :html) + get :logs_tree, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: 'master', + path: 'foo/bar/baz.html', format: format + end + + def xhr_get(format = :html) + xhr :get, :logs_tree, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: 'master', + path: 'foo/bar/baz.html', format: format + end + + it 'never throws MissingTemplate' do + expect { default_get }.not_to raise_error + expect { xhr_get }.not_to raise_error + end + + it 'renders 404 for non-JS requests' do + xhr_get + + expect(response).to be_not_found + end + + it 'renders JS' do + xhr_get(:js) + expect(response).to be_success + end + end +end diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb new file mode 100644 index 00000000000..91856ed0cc0 --- /dev/null +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -0,0 +1,65 @@ +require "spec_helper" + +describe Projects::RepositoriesController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + describe "GET archive" do + before do + sign_in(user) + project.team << [user, :developer] + + allow(ArchiveRepositoryService).to receive(:new).and_return(service) + end + + let(:service) { ArchiveRepositoryService.new(project, "master", "zip") } + + it "executes ArchiveRepositoryService" do + expect(ArchiveRepositoryService).to receive(:new).with(project, "master", "zip") + expect(service).to receive(:execute) + + get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + end + + context "when the service raises an error" do + + before do + allow(service).to receive(:execute).and_raise("Archive failed") + end + + it "renders Not Found" do + get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + + expect(response.status).to eq(404) + end + end + + context "when the service doesn't return a path" do + + before do + allow(service).to receive(:execute).and_return(nil) + end + + it "reloads the page" do + get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + + expect(response).to redirect_to(archive_namespace_project_repository_path(project.namespace, project, ref: "master", format: "zip")) + end + end + + context "when the service returns a path" do + + let(:path) { Rails.root.join("spec/fixtures/dk.png").to_s } + + before do + allow(service).to receive(:execute).and_return(path) + end + + it "sends the file" do + get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + + expect(response.body).to eq(File.binread(path)) + end + end + end +end diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb new file mode 100644 index 00000000000..f51abfedae5 --- /dev/null +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -0,0 +1,280 @@ +require('spec_helper') + +describe Projects::UploadsController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } + let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + + describe "POST #create" do + before do + sign_in(user) + project.team << [user, :developer] + end + + context "without params['file']" do + it "returns an error" do + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + format: :json + expect(response.status).to eq(422) + end + end + + context 'with valid image' do + before do + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + file: jpg, + format: :json + end + + it 'returns a content with original filename, new link, and correct type.' do + expect(response.body).to match '\"alt\":\"rails_sample\"' + expect(response.body).to match "\"url\":\"http://localhost/#{project.path_with_namespace}/uploads" + expect(response.body).to match '\"is_image\":true' + end + end + + context 'with valid non-image file' do + before do + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + file: txt, + format: :json + end + + it 'returns a content with original filename, new link, and correct type.' do + expect(response.body).to match '\"alt\":\"doc_sample.txt\"' + expect(response.body).to match "\"url\":\"http://localhost/#{project.path_with_namespace}/uploads" + expect(response.body).to match '\"is_image\":false' + end + end + end + + describe "GET #show" do + let(:go) do + get :show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + secret: "123456", + filename: "image.jpg" + end + + context "when the project is public" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + context "when not signed in" do + context "when the file exists" do + before do + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) + end + + it "responds with status 200" do + go + + expect(response.status).to eq(200) + end + end + + context "when the file doesn't exist" do + it "responds with status 404" do + go + + expect(response.status).to eq(404) + end + end + end + + context "when signed in" do + before do + sign_in(user) + end + + context "when the file exists" do + before do + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) + end + + it "responds with status 200" do + go + + expect(response.status).to eq(200) + end + end + + context "when the file doesn't exist" do + it "responds with status 404" do + go + + 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 not signed in" do + context "when the file exists" do + before do + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) + end + + context "when the file is an image" do + before do + allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) + end + + it "responds with status 200" do + go + + expect(response.status).to eq(200) + end + end + + context "when the file is not an image" do + it "redirects to the sign in page" do + go + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context "when the file doesn't exist" do + it "redirects to the sign in page" do + go + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context "when signed in" do + before do + sign_in(user) + 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 + + context "when the file exists" do + before do + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) + end + + context "when the file is an image" do + before do + allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) + end + + it "responds with status 200" do + go + + expect(response.status).to eq(200) + end + end + + context "when the file is not an image" do + it "redirects to the sign in page" do + go + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context "when the file doesn't exist" do + it "redirects to the sign in page" do + go + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context "when the user isn't blocked" do + context "when the file exists" do + before do + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) + end + + it "responds with status 200" do + go + + expect(response.status).to eq(200) + end + end + + context "when the file doesn't exist" do + it "responds with status 404" do + go + + expect(response.status).to eq(404) + end + end + end + end + + context "when the user doesn't have access to the project" do + context "when the file exists" do + before do + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) + end + + context "when the file is an image" do + before do + allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) + end + + it "responds with status 200" do + go + + expect(response.status).to eq(200) + end + end + + context "when the file is not an image" do + it "responds with status 404" do + go + + expect(response.status).to eq(404) + end + end + end + + context "when the file doesn't exist" do + it "responds with status 404" do + go + + expect(response.status).to eq(404) + end + end + end + end + end + end +end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 71bc49787cc..a1b82a32150 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -7,56 +7,41 @@ describe ProjectsController do let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } - describe "POST #upload_image" do - before do - sign_in(user) - project.team << [user, :developer] - end + describe "GET show" do - context "without params['markdown_img']" do - it "returns an error" do - post :upload_image, id: project.to_param, format: :json - expect(response.status).to eq(422) - end - end + context "when requested by `go get`" do + render_views - context "with invalid file" do - before do - post :upload_image, id: project.to_param, markdown_img: txt, format: :json - end + it "renders the go-import meta tag" do + get :show, "go-get" => "1", namespace_id: "bogus_namespace", id: "bogus_project" - it "returns an error" do - expect(response.status).to eq(422) - end - end - - context "with valid file" do - before do - post :upload_image, id: project.to_param, markdown_img: jpg, format: :json - end - - it "returns a content with original filename and new link." do - expect(response.body).to match "\"alt\":\"rails_sample\"" - expect(response.body).to match "\"url\":\"http://test.host/uploads/#{project.path_with_namespace}" + expect(response.body).to include("name='go-import'") + + content = "localhost/bogus_namespace/bogus_project git http://localhost/bogus_namespace/bogus_project.git" + expect(response.body).to include("content='#{content}'") end end end - + describe "POST #toggle_star" do it "toggles star if user is signed in" do sign_in(user) - expect(user.starred?(public_project)).to be_false - post :toggle_star, id: public_project.to_param - expect(user.starred?(public_project)).to be_true - post :toggle_star, id: public_project.to_param - expect(user.starred?(public_project)).to be_false + expect(user.starred?(public_project)).to be_falsey + post(:toggle_star, namespace_id: public_project.namespace.to_param, + id: public_project.to_param) + expect(user.starred?(public_project)).to be_truthy + post(:toggle_star, namespace_id: public_project.namespace.to_param, + id: public_project.to_param) + expect(user.starred?(public_project)).to be_falsey end it "does nothing if user is not signed in" do - post :toggle_star, id: public_project.to_param - expect(user.starred?(public_project)).to be_false - post :toggle_star, id: public_project.to_param - expect(user.starred?(public_project)).to be_false + post(:toggle_star, namespace_id: project.namespace.to_param, + id: public_project.to_param) + expect(user.starred?(public_project)).to be_falsey + post(:toggle_star, namespace_id: project.namespace.to_param, + id: public_project.to_param) + expect(user.starred?(public_project)).to be_falsey end end end diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb new file mode 100644 index 00000000000..abbbf6855fc --- /dev/null +++ b/spec/controllers/root_controller_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe RootController do + describe 'GET show' do + context 'with a user' do + let(:user) { create(:user) } + + before do + sign_in(user) + allow(subject).to receive(:current_user).and_return(user) + end + + context 'who has customized their dashboard setting' do + before do + user.update_attribute(:dashboard, 'stars') + end + + it 'redirects to their specified dashboard' do + get :show + expect(response).to redirect_to starred_dashboard_projects_path + end + end + + context 'who uses the default dashboard setting' do + it 'renders the default dashboard' do + get :show + expect(response).to render_template 'dashboard/show' + end + end + end + end +end diff --git a/spec/controllers/tree_controller_spec.rb b/spec/controllers/tree_controller_spec.rb index 8147fb0e6fb..7b219819bbc 100644 --- a/spec/controllers/tree_controller_spec.rb +++ b/spec/controllers/tree_controller_spec.rb @@ -9,8 +9,8 @@ describe Projects::TreeController do project.team << [user, :master] - project.stub(:branches).and_return(['master', 'foo/bar/baz']) - project.stub(:tags).and_return(['v1.0.0', 'v2.0.0']) + allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz']) + allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0']) controller.instance_variable_set(:@project, project) end @@ -18,26 +18,29 @@ describe Projects::TreeController do # Make sure any errors accessing the tree in our views bubble up to this spec render_views - before { get :show, project_id: project.to_param, id: id } + before do + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: id) + end context "valid branch, no path" do let(:id) { 'master' } - it { should respond_with(:success) } + it { is_expected.to respond_with(:success) } end context "valid branch, valid path" do let(:id) { 'master/encoding/' } - it { should respond_with(:success) } + it { is_expected.to respond_with(:success) } end context "valid branch, invalid path" do let(:id) { 'master/invalid-path/' } - it { should respond_with(:not_found) } + it { is_expected.to respond_with(:not_found) } end context "invalid branch, valid path" do let(:id) { 'invalid-branch/encoding/' } - it { should respond_with(:not_found) } + it { is_expected.to respond_with(:not_found) } end end @@ -45,12 +48,17 @@ describe Projects::TreeController do render_views before do - get :show, project_id: project.to_param, id: id + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: id) end context 'redirect to blob' do let(:id) { 'master/README.md' } - it { should redirect_to("/#{project.path_with_namespace}/blob/master/README.md") } + it 'redirects' do + redirect_url = "/#{project.path_with_namespace}/blob/master/README.md" + expect(subject). + to redirect_to(redirect_url) + end end end end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb new file mode 100644 index 00000000000..0f9780356b1 --- /dev/null +++ b/spec/controllers/uploads_controller_spec.rb @@ -0,0 +1,296 @@ +require 'spec_helper' + +describe UploadsController do + let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + + describe "GET show" do + context "when viewing a user avatar" do + context "when signed in" do + before do + sign_in(user) + end + + context "when the user is blocked" do + before do + user.block + end + + it "redirects to the sign in page" do + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when not signed in" do + it "responds with status 200" do + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when viewing a project avatar" do + let!(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + + context "when the project is public" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + context "when not signed in" do + it "responds with status 200" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + it "responds with status 200" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the project is private" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + context "when not signed in" do + it "redirects to the sign in page" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when signed in" do + before do + sign_in(user) + 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 "redirects to the sign in page" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response.status).to eq(404) + end + end + end + end + end + + context "when viewing a group avatar" do + let!(:group) { create(:group, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let!(:project) { create(:project, namespace: group) } + + context "when the group has public projects" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + context "when not signed in" do + it "responds with status 200" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + it "responds with status 200" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the project doesn't have public projects" do + context "when not signed in" do + it "redirects to the sign in page" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when signed in" do + before do + sign_in(user) + 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 "redirects to the sign in page" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response.status).to eq(404) + end + end + end + end + end + + context "when viewing a note attachment" do + let!(:note) { create(:note, :with_attachment) } + let(:project) { note.project } + + context "when the project is public" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + context "when not signed in" do + it "responds with status 200" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + it "responds with status 200" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the project is private" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + context "when not signed in" do + it "redirects to the sign in page" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when signed in" do + before do + sign_in(user) + 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 "redirects to the sign in page" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response.status).to eq(404) + end + end + end + end + end + end +end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb new file mode 100644 index 00000000000..d47a37914df --- /dev/null +++ b/spec/controllers/users_controller_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe UsersController do + let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } + + before do + sign_in(user) + end + + describe 'GET #show' do + render_views + + it 'renders the show template' do + get :show, username: user.username + expect(response.status).to eq(200) + expect(response).to render_template('show') + end + end + + describe 'GET #calendar' do + it 'renders calendar' do + get :calendar, username: user.username + expect(response).to render_template('calendar') + end + end + + describe 'GET #calendar_activities' do + let!(:project) { create(:project) } + let!(:user) { create(:user) } + + before do + allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id]) + project.team << [user, :developer] + end + + it 'assigns @calendar_date' do + get :calendar_activities, username: user.username, date: '2014-07-31' + expect(assigns(:calendar_date)).to eq(Date.parse('2014-07-31')) + end + + it 'renders calendar_activities' do + get :calendar_activities, username: user.username + expect(response).to render_template('calendar_activities') + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index 15899d8c3c4..578a2e4dc69 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -2,31 +2,51 @@ include ActionDispatch::TestProcess FactoryGirl.define do sequence :sentence, aliases: [:title, :content] do - Faker::Lorem.sentence + FFaker::Lorem.sentence end - sequence :name, aliases: [:file_name] do - Faker::Name.name + sequence :name do + FFaker::Name.name end - sequence(:url) { Faker::Internet.uri('http') } + sequence :file_name do + FFaker::Internet.user_name + end + + sequence(:url) { FFaker::Internet.uri('http') } factory :user, aliases: [:author, :assignee, :owner, :creator] do - email { Faker::Internet.email } + email { FFaker::Internet.email } name - sequence(:username) { |n| "#{Faker::Internet.user_name}#{n}" } + sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" } password "12345678" - password_confirmation { password } confirmed_at { Time.now } confirmation_token { nil } + can_create_group true trait :admin do admin true end - trait :ldap do - provider 'ldapmain' - extern_uid 'my-ldap-id' + trait :two_factor do + before(:create) do |user| + user.two_factor_enabled = true + user.otp_secret = User.generate_otp_secret(32) + end + end + + factory :omniauth_user do + ignore do + extern_uid '123456' + provider 'ldapmain' + end + + after(:create) do |user, evaluator| + user.identities << create(:identity, + provider: evaluator.provider, + extern_uid: evaluator.extern_uid + ) + end end factory :admin, traits: [:admin] @@ -89,21 +109,12 @@ FactoryGirl.define do user end - factory :key_with_a_space_in_the_middle do - key do - "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa ++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" - end - end - factory :another_key do key do "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ" end - end - factory :invalid_key do - key do - "ssh-rsa this_is_invalid_key==" + factory :another_deploy_key, class: 'DeployKey' do end end end @@ -111,12 +122,12 @@ FactoryGirl.define do factory :email do user email do - Faker::Internet.email('alias') + FFaker::Internet.email('alias') end factory :another_email do email do - Faker::Internet.email('another.alias') + FFaker::Internet.email('another.alias') end end end @@ -182,4 +193,9 @@ FactoryGirl.define do deploy_key project end + + factory :identity do + provider 'ldapmain' + extern_uid 'my-ldap-id' + end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 0ae8ea5f878..77cd37c22d9 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -18,6 +18,7 @@ # iid :integer # description :text # position :integer default(0) +# locked_at :datetime # FactoryGirl.define do @@ -39,7 +40,7 @@ FactoryGirl.define do source_branch "master" target_branch "feature" - merge_status :can_be_merged + merge_status "can_be_merged" trait :with_diffs do end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 83d0cc62dbf..e1009d5916e 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -25,14 +25,16 @@ FactoryGirl.define do note "Note" author - factory :note_on_commit, traits: [:on_commit] - factory :note_on_commit_diff, traits: [:on_commit, :on_diff] - factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] - factory :note_on_merge_request, traits: [:on_merge_request] + factory :note_on_commit, traits: [:on_commit] + factory :note_on_commit_diff, traits: [:on_commit, :on_diff] + 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_project_snippet, traits: [:on_project_snippet] + factory :system_note, traits: [:system] trait :on_commit do - project factory: :project + project commit_id RepoHelpers.sample_commit.id noteable_type "Commit" end @@ -42,7 +44,7 @@ FactoryGirl.define do end trait :on_merge_request do - project factory: :project + project noteable_id 1 noteable_type "MergeRequest" end @@ -52,6 +54,15 @@ FactoryGirl.define do noteable_type "Issue" end + trait :on_project_snippet do + noteable_id 1 + noteable_type "Snippet" + end + + trait :system do + system true + end + trait :with_attachment do attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 60eb73e4a95..102678a1d74 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -24,6 +24,9 @@ # 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) +# avatar :string(255) # FactoryGirl.define do @@ -73,8 +76,44 @@ FactoryGirl.define do end end + factory :forked_project_with_submodules, parent: :empty_project do + path { 'forked-gitlabhq' } + + after :create do |project| + TestEnv.copy_forked_repo_with_submodules(project) + end + end + factory :redmine_project, parent: :project do - issues_tracker { "redmine" } - issues_tracker_id { "project_name_in_redmine" } + after :create do |project| + project.create_redmine_service( + 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' + } + ) + + project.issues_tracker = 'redmine' + project.issues_tracker_id = 'project_name_in_redmine' + end + end + + factory :jira_project, parent: :project do + after :create do |project| + project.create_jira_service( + active: true, + properties: { + 'title' => 'JIRA tracker', + 'project_url' => 'http://jira.example/issues/?jql=project=A', + 'issues_url' => 'http://jira.example/browse/:id', + 'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa' + } + ) + + project.issues_tracker = 'jira' + project.issues_tracker_id = 'project_name_in_jira' + end end end diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb index 66bef0761c7..457859dedaf 100644 --- a/spec/factories_spec.rb +++ b/spec/factories_spec.rb @@ -1,15 +1,9 @@ require 'spec_helper' -INVALID_FACTORIES = [ - :key_with_a_space_in_the_middle, - :invalid_key, -] - FactoryGirl.factories.map(&:name).each do |factory_name| - next if INVALID_FACTORIES.include?(factory_name) describe "#{factory_name} factory" do it 'should be valid' do - build(factory_name).should be_valid + expect(build(factory_name)).to be_valid end end end diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index b557567bd04..7265cdac7a7 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -12,29 +12,29 @@ describe "Admin::Hooks", feature: true do describe "GET /admin/hooks" do it "should be ok" do visit admin_root_path - within ".main-nav" do + page.within ".sidebar-wrapper" do click_on "Hooks" end - current_path.should == admin_hooks_path + expect(current_path).to eq(admin_hooks_path) end it "should have hooks list" do visit admin_hooks_path - page.should have_content(@system_hook.url) + expect(page).to have_content(@system_hook.url) end end describe "New Hook" do before do - @url = Faker::Internet.uri("http") + @url = FFaker::Internet.uri("http") visit admin_hooks_path fill_in "hook_url", with: @url expect { click_button "Add System Hook" }.to change(SystemHook, :count).by(1) end it "should open new hook popup" do - current_path.should == admin_hooks_path - page.should have_content(@url) + expect(current_path).to eq(admin_hooks_path) + expect(page).to have_content(@url) end end @@ -45,7 +45,7 @@ describe "Admin::Hooks", feature: true do click_link "Test Hook" end - it { current_path.should == admin_hooks_path } + it { expect(current_path).to eq(admin_hooks_path) } end end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 3b3d027ab75..101d955d693 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -8,27 +8,27 @@ describe "Admin::Projects", feature: true do describe "GET /admin/projects" do before do - visit admin_projects_path + visit admin_namespaces_projects_path end it "should be ok" do - current_path.should == admin_projects_path + expect(current_path).to eq(admin_namespaces_projects_path) end it "should have projects list" do - page.should have_content(@project.name) + expect(page).to have_content(@project.name) end end describe "GET /admin/projects/:id" do before do - visit admin_projects_path + visit admin_namespaces_projects_path click_link "#{@project.name}" end it "should have project info" do - page.should have_content(@project.path) - page.should have_content(@project.name) + expect(page).to have_content(@project.path) + expect(page).to have_content(@project.name) end end end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 82da19746f8..7f5cb30cb94 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -9,12 +9,12 @@ describe "Admin::Users", feature: true do end it "should be ok" do - current_path.should == admin_users_path + expect(current_path).to eq(admin_users_path) end it "should have users list" do - page.should have_content(@user.email) - page.should have_content(@user.name) + expect(page).to have_content(@user.email) + expect(page).to have_content(@user.name) end end @@ -32,43 +32,65 @@ describe "Admin::Users", feature: true do it "should apply defaults to user" do click_button "Create user" - user = User.last - user.projects_limit.should == Gitlab.config.gitlab.default_projects_limit - user.can_create_group.should == Gitlab.config.gitlab.default_can_create_group + user = User.find_by(username: 'bang') + expect(user.projects_limit). + to eq(Gitlab.config.gitlab.default_projects_limit) + expect(user.can_create_group). + to eq(Gitlab.config.gitlab.default_can_create_group) end it "should create user with valid data" do click_button "Create user" - user = User.last - user.name.should == "Big Bang" - user.email.should == "bigbang@mail.com" + user = User.find_by(username: 'bang') + expect(user.name).to eq('Big Bang') + expect(user.email).to eq('bigbang@mail.com') end it "should call send mail" do - Notify.should_receive(:new_user_email) + expect(Notify).to receive(:new_user_email) click_button "Create user" end it "should send valid email to user with email & password" do click_button "Create user" - user = User.last + user = User.find_by(username: 'bang') email = ActionMailer::Base.deliveries.last - email.subject.should have_content("Account was created") - email.text_part.body.should have_content(user.email) - email.text_part.body.should have_content('password') + expect(email.subject).to have_content('Account was created') + expect(email.text_part.body).to have_content(user.email) + expect(email.text_part.body).to have_content('password') end end describe "GET /admin/users/:id" do - before do + it "should have user info" do visit admin_users_path - click_link "#{@user.name}" + click_link @user.name + + expect(page).to have_content(@user.email) + expect(page).to have_content(@user.name) end - it "should have user info" do - page.should have_content(@user.email) - page.should have_content(@user.name) + describe 'Two-factor Authentication status' do + it 'shows when enabled' do + @user.update_attribute(:two_factor_enabled, true) + + visit admin_user_path(@user) + + expect_two_factor_status('Enabled') + end + + it 'shows when disabled' do + visit admin_user_path(@user) + + expect_two_factor_status('Disabled') + end + + def expect_two_factor_status(status) + page.within('.two-factor-status') do + expect(page).to have_content(status) + end + end end end @@ -80,8 +102,8 @@ describe "Admin::Users", feature: true do end it "should have user edit page" do - page.should have_content("Name") - page.should have_content("Password") + expect(page).to have_content('Name') + expect(page).to have_content('Password') end describe "Update user" do @@ -93,14 +115,14 @@ describe "Admin::Users", feature: true do end it "should show page with new data" do - page.should have_content("bigbang@mail.com") - page.should have_content("Big Bang") + expect(page).to have_content('bigbang@mail.com') + expect(page).to have_content('Big Bang') end it "should change user entry" do @simple_user.reload - @simple_user.name.should == "Big Bang" - @simple_user.is_admin?.should be_true + expect(@simple_user.name).to eq('Big Bang') + expect(@simple_user.is_admin?).to be_truthy end end end diff --git a/spec/features/admin/security_spec.rb b/spec/features/admin/security_spec.rb index 21b0d8b965e..175fa9d4647 100644 --- a/spec/features/admin/security_spec.rb +++ b/spec/features/admin/security_spec.rb @@ -2,26 +2,26 @@ require 'spec_helper' describe "Admin::Projects", feature: true do describe "GET /admin/projects" do - subject { admin_projects_path } + subject { admin_namespaces_projects_path } - it { should be_allowed_for :admin } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /admin/users" do subject { admin_users_path } - it { should be_allowed_for :admin } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /admin/hooks" do subject { admin_hooks_path } - it { should be_allowed_for :admin } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end end diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index 187f2ffcffd..b710cb3c72f 100644 --- a/spec/features/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb @@ -17,12 +17,13 @@ describe "Dashboard Issues Feed", feature: true do it "should render atom feed via private token" do visit issues_dashboard_path(:atom, private_token: user.private_token) - response_headers['Content-Type'].should have_content("application/atom+xml") - body.should have_selector("title", text: "#{user.name} issues") - body.should have_selector("author email", text: issue1.author_email) - body.should have_selector("entry summary", text: issue1.title) - body.should have_selector("author email", text: issue2.author_email) - body.should have_selector("entry summary", text: issue2.title) + expect(response_headers['Content-Type']). + to have_content('application/atom+xml') + expect(body).to have_selector('title', text: "#{user.name} issues") + expect(body).to have_selector('author email', text: issue1.author_email) + expect(body).to have_selector('entry summary', text: issue1.title) + expect(body).to have_selector('author email', text: issue2.author_email) + expect(body).to have_selector('entry summary', text: issue2.title) end end end diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index a7f87906b2d..ad157d742ff 100644 --- a/spec/features/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb @@ -2,12 +2,12 @@ require 'spec_helper' describe "Dashboard Feed", feature: true do describe "GET /" do - let!(:user) { create(:user) } + let!(:user) { create(:user, name: "Jonh") } context "projects atom feed via private token" do it "should render projects atom feed" do visit dashboard_path(:atom, private_token: user.private_token) - body.should have_selector("feed title") + expect(body).to have_selector('feed title') end end @@ -24,11 +24,12 @@ describe "Dashboard Feed", feature: true do end it "should have issue opened event" do - body.should have_content("#{user.name} opened issue ##{issue.iid}") + expect(body).to have_content("#{user.name} opened issue ##{issue.iid}") end it "should have issue comment event" do - body.should have_content("#{user.name} commented on issue ##{issue.iid}") + expect(body). + to have_content("#{user.name} commented on issue ##{issue.iid}") end end end diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index 453dca69094..baa7814e96a 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -1,33 +1,36 @@ require 'spec_helper' -describe "Issues Feed", feature: true do - describe "GET /issues" do +describe 'Issues Feed', feature: true do + describe 'GET /issues' do let!(:user) { create(:user) } let!(:project) { create(:project) } let!(:issue) { create(:issue, author: user, project: project) } before { project.team << [user, :developer] } - context "when authenticated" do - it "should render atom feed" do + context 'when authenticated' do + it 'should render atom feed' do login_with user - visit project_issues_path(project, :atom) + visit namespace_project_issues_path(project.namespace, project, :atom) - response_headers['Content-Type'].should have_content("application/atom+xml") - body.should have_selector("title", text: "#{project.name} issues") - body.should have_selector("author email", text: issue.author_email) - body.should have_selector("entry summary", text: issue.title) + expect(response_headers['Content-Type']). + to have_content('application/atom+xml') + expect(body).to have_selector('title', text: "#{project.name} issues") + expect(body).to have_selector('author email', text: issue.author_email) + expect(body).to have_selector('entry summary', text: issue.title) end end - context "when authenticated via private token" do - it "should render atom feed" do - visit project_issues_path(project, :atom, private_token: user.private_token) + context 'when authenticated via private token' do + it 'should render atom feed' do + visit namespace_project_issues_path(project.namespace, project, :atom, + private_token: user.private_token) - response_headers['Content-Type'].should have_content("application/atom+xml") - body.should have_selector("title", text: "#{project.name} issues") - body.should have_selector("author email", text: issue.author_email) - body.should have_selector("entry summary", text: issue.title) + expect(response_headers['Content-Type']). + to have_content('application/atom+xml') + expect(body).to have_selector('title', text: "#{project.name} issues") + expect(body).to have_selector('author email', text: issue.author_email) + expect(body).to have_selector('entry summary', text: issue.title) end end end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb new file mode 100644 index 00000000000..770ac04c2c5 --- /dev/null +++ b/spec/features/atom/users_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe "User Feed", feature: true do + describe "GET /" do + let!(:user) { create(:user) } + + context 'user atom feed via private token' do + it "should render user atom feed" do + visit user_path(user, :atom, private_token: user.private_token) + expect(body).to have_selector('feed title') + end + end + + context 'feed content' do + let(:project) { create(:project) } + let(:issue) do + create(:issue, project: project, + author: user, description: "Houston, we have a bug!\n\n***\n\nI guess.") + end + let(:note) do + create(:note, noteable: issue, author: user, + note: 'Bug confirmed :+1:', project: project) + end + let(:merge_request) do + create(:merge_request, + title: 'Fix bug', author: user, + source_project: project, target_project: project, + description: "Here is the fix: ") + end + + before do + project.team << [user, :master] + issue_event(issue, user) + note_event(note, user) + merge_request_event(merge_request, user) + visit user_path(user, :atom, private_token: user.private_token) + end + + it 'should have issue opened event' do + expect(body).to have_content("#{safe_name} opened issue ##{issue.iid}") + end + + it 'should have issue comment event' do + expect(body). + to have_content("#{safe_name} commented on issue ##{issue.iid}") + end + + it 'should have XHTML summaries in issue descriptions' do + expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p>I guess/ + end + + it 'should have XHTML summaries in notes' do + expect(body).to match /Bug confirmed <img[^>]*\/>/ + end + + it 'should have XHTML summaries in merge request descriptions' do + expect(body).to match /Here is the fix: <img[^>]*\/>/ + end + end + end + + def issue_event(issue, user) + EventCreateService.new.open_issue(issue, user) + end + + def note_event(note, user) + EventCreateService.new.leave_note(note, user) + end + + def merge_request_event(request, user) + EventCreateService.new.open_mr(request, user) + end + + def safe_name + html_escape(user.name) + end +end diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb index 9f50d1c9738..0c1bc53cdb5 100644 --- a/spec/features/gitlab_flavored_markdown_spec.rb +++ b/spec/features/gitlab_flavored_markdown_spec.rb @@ -11,10 +11,11 @@ describe "GitLab Flavored Markdown", feature: true do end before do - Commit.any_instance.stub(title: "fix ##{issue.iid}\n\nask @#{fred.username} for details") + allow_any_instance_of(Commit).to receive(:title). + and_return("fix #{issue.to_reference}\n\nask #{fred.to_reference} for details") end - let(:commit) { project.repository.commit } + let(:commit) { project.commit } before do login_as :user @@ -23,27 +24,27 @@ describe "GitLab Flavored Markdown", feature: true do describe "for commits" do it "should render title in commits#index" do - visit project_commits_path(project, 'master', limit: 1) + visit namespace_project_commits_path(project.namespace, project, 'master', limit: 1) - page.should have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render title in commits#show" do - visit project_commit_path(project, commit) + visit namespace_project_commit_path(project.namespace, project, commit) - page.should have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render description in commits#show" do - visit project_commit_path(project, commit) + visit namespace_project_commit_path(project.namespace, project, commit) - page.should have_link("@#{fred.username}") + expect(page).to have_link(fred.to_reference) end it "should render title in repositories#branches" do - visit project_branches_path(project) + visit namespace_project_branches_path(project.namespace, project) - page.should have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end end @@ -57,45 +58,45 @@ describe "GitLab Flavored Markdown", feature: true do author: @user, assignee: @user, project: project, - title: "fix ##{@other_issue.iid}", - description: "ask @#{fred.username} for details") + title: "fix #{@other_issue.to_reference}", + description: "ask #{fred.to_reference} for details") end it "should render subject in issues#index" do - visit project_issues_path(project) + visit namespace_project_issues_path(project.namespace, project) - page.should have_link("##{@other_issue.iid}") + expect(page).to have_link(@other_issue.to_reference) end it "should render subject in issues#show" do - visit project_issue_path(project, @issue) + visit namespace_project_issue_path(project.namespace, project, @issue) - page.should have_link("##{@other_issue.iid}") + expect(page).to have_link(@other_issue.to_reference) end it "should render details in issues#show" do - visit project_issue_path(project, @issue) + visit namespace_project_issue_path(project.namespace, project, @issue) - page.should have_link("@#{fred.username}") + expect(page).to have_link("@#{fred.username}") end end describe "for merge requests" do before do - @merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix ##{issue.iid}") + @merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix #{issue.to_reference}") end it "should render title in merge_requests#index" do - visit project_merge_requests_path(project) + visit namespace_project_merge_requests_path(project.namespace, project) - page.should have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render title in merge_requests#show" do - visit project_merge_request_path(project, @merge_request) + visit namespace_project_merge_request_path(project.namespace, project, @merge_request) - page.should have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end end @@ -104,26 +105,26 @@ describe "GitLab Flavored Markdown", feature: true do before do @milestone = create(:milestone, project: project, - title: "fix ##{issue.iid}", - description: "ask @#{fred.username} for details") + title: "fix #{issue.to_reference}", + description: "ask #{fred.to_reference} for details") end it "should render title in milestones#index" do - visit project_milestones_path(project) + visit namespace_project_milestones_path(project.namespace, project) - page.should have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render title in milestones#show" do - visit project_milestone_path(project, @milestone) + visit namespace_project_milestone_path(project.namespace, project, @milestone) - page.should have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render description in milestones#show" do - visit project_milestone_path(project, @milestone) + visit namespace_project_milestone_path(project.namespace, project, @milestone) - page.should have_link("@#{fred.username}") + expect(page).to have_link(fred.to_reference) end end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb new file mode 100644 index 00000000000..edc1c63a0aa --- /dev/null +++ b/spec/features/groups_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +feature 'Group' do + describe 'description' do + let(:group) { create(:group) } + let(:path) { group_path(group) } + + before do + login_as(:admin) + end + + it 'parses Markdown' do + group.update_attribute(:description, 'This is **my** group') + visit path + expect(page).to have_css('.description > p > strong') + end + + it 'passes through html-pipeline' do + group.update_attribute(:description, 'This group is the :poop:') + visit path + expect(page).to have_css('.description > p > img') + end + + it 'sanitizes unwanted tags' do + group.update_attribute(:description, '# Group Description') + visit path + expect(page).not_to have_css('.description h1') + end + + it 'permits `rel` attribute on links' do + group.update_attribute(:description, 'https://google.com/') + visit path + expect(page).to have_css('.description a[rel]') + end + end +end diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb new file mode 100644 index 00000000000..8c6b669ce78 --- /dev/null +++ b/spec/features/help_pages_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe 'Help Pages', feature: true do + describe 'Show SSH page' do + before do + login_as :user + end + it 'replace the variable $your_email with the email of the user' do + visit help_page_path('ssh', 'README') + expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"") + end + end +end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 26607b0090c..1f2675044d3 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' -describe "Issues", feature: true do +describe 'Issues', feature: true do + include SortingHelper + let(:project) { create(:project) } before do @@ -10,7 +12,7 @@ describe "Issues", feature: true do project.team << [[@user, user2], :developer] end - describe "Edit issue" do + describe 'Edit issue' do let!(:issue) do create(:issue, author: @user, @@ -19,34 +21,38 @@ describe "Issues", feature: true do end before do - visit project_issues_path(project) + visit edit_namespace_project_issue_path(project.namespace, project, issue) click_link "Edit" end - it "should open new issue popup" do - page.should have_content("Issue ##{issue.iid}") + it 'should open new issue popup' do + expect(page).to have_content("Issue ##{issue.iid}") end - describe "fill in" do + describe 'fill in' do before do - fill_in "issue_title", with: "bug 345" - fill_in "issue_description", with: "bug description" + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' end - it { expect { click_button "Save changes" }.to_not change {Issue.count} } + it 'does not change issue count' do + expect { + click_button 'Save changes' + }.not_to change { Issue.count } + end - it "should update issue fields" do - click_button "Save changes" + it 'should update issue fields' do + click_button 'Save changes' - page.should have_content @user.name - page.should have_content "bug 345" - page.should have_content project.name + expect(page).to have_content @user.name + expect(page).to have_content 'bug 345' + expect(page).to have_content project.name end end end - describe "Editing issue assignee" do + describe 'Editing issue assignee' do let!(:issue) do create(:issue, author: @user, @@ -54,23 +60,23 @@ describe "Issues", feature: true do project: project) end - it 'allows user to select unasigned', :js => true do - visit edit_project_issue_path(project, issue) + it 'allows user to select unasigned', js: true do + visit edit_namespace_project_issue_path(project.namespace, project, issue) - page.should have_content "Assign to #{@user.name}" + expect(page).to have_content "Assign to #{@user.name}" first('#s2id_issue_assignee_id').click sleep 2 # wait for ajax stuff to complete first('.user-result').click - click_button "Save changes" + click_button 'Save changes' - page.should have_content "Assignee: Select assignee" - issue.reload.assignee.should be_nil + expect(page).to have_content 'Assignee: none' + expect(issue.reload.assignee).to be_nil end end - describe "Filter issue" do + describe 'Filter issue' do before do ['foobar', 'barbaz', 'gitlab'].each do |title| create(:issue, @@ -80,7 +86,7 @@ describe "Issues", feature: true do title: title) end - @issue = Issue.first # with title 'foobar' + @issue = Issue.find_by(title: 'foobar') @issue.milestone = create(:milestone, project: project) @issue.assignee = nil @issue.save @@ -88,75 +94,79 @@ describe "Issues", feature: true do let(:issue) { @issue } - it "should allow filtering by issues with no specified milestone" do - visit project_issues_path(project, milestone_id: '0') + it 'should allow filtering by issues with no specified milestone' do + visit namespace_project_issues_path(project.namespace, project, milestone_title: IssuableFinder::NONE) - page.should_not have_content 'foobar' - page.should have_content 'barbaz' - page.should have_content 'gitlab' + expect(page).not_to have_content 'foobar' + expect(page).to have_content 'barbaz' + expect(page).to have_content 'gitlab' end - it "should allow filtering by a specified milestone" do - visit project_issues_path(project, milestone_id: issue.milestone.id) + it 'should allow filtering by a specified milestone' do + visit namespace_project_issues_path(project.namespace, project, milestone_title: issue.milestone.title) - page.should have_content 'foobar' - page.should_not have_content 'barbaz' - page.should_not have_content 'gitlab' + expect(page).to have_content 'foobar' + expect(page).not_to have_content 'barbaz' + expect(page).not_to have_content 'gitlab' end - it "should allow filtering by issues with no specified assignee" do - visit project_issues_path(project, assignee_id: '0') + it 'should allow filtering by issues with no specified assignee' do + visit namespace_project_issues_path(project.namespace, project, assignee_id: IssuableFinder::NONE) - page.should have_content 'foobar' - page.should_not have_content 'barbaz' - page.should_not have_content 'gitlab' + expect(page).to have_content 'foobar' + expect(page).not_to have_content 'barbaz' + expect(page).not_to have_content 'gitlab' end - it "should allow filtering by a specified assignee" do - visit project_issues_path(project, assignee_id: @user.id) + it 'should allow filtering by a specified assignee' do + visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) - page.should_not have_content 'foobar' - page.should have_content 'barbaz' - page.should have_content 'gitlab' + expect(page).not_to have_content 'foobar' + expect(page).to have_content 'barbaz' + expect(page).to have_content 'gitlab' end end describe 'filter issue' do titles = ['foo','bar','baz'] titles.each_with_index do |title, index| - let!(title.to_sym) { create(:issue, title: title, project: project, created_at: Time.now - (index * 60)) } + let!(title.to_sym) do + create(:issue, title: title, + project: project, + created_at: Time.now - (index * 60)) + end end let(:newer_due_milestone) { create(:milestone, due_date: '2013-12-11') } let(:later_due_milestone) { create(:milestone, due_date: '2013-12-12') } it 'sorts by newest' do - visit project_issues_path(project, sort: 'newest') + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_created) - first_issue.should include("foo") - last_issue.should include("baz") + expect(first_issue).to include('foo') + expect(last_issue).to include('baz') end it 'sorts by oldest' do - visit project_issues_path(project, sort: 'oldest') + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_created) - first_issue.should include("baz") - last_issue.should include("foo") + expect(first_issue).to include('baz') + expect(last_issue).to include('foo') end it 'sorts by most recently updated' do baz.updated_at = Time.now + 100 baz.save - visit project_issues_path(project, sort: 'recently_updated') + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_updated) - first_issue.should include("baz") + expect(first_issue).to include('baz') end it 'sorts by least recently updated' do baz.updated_at = Time.now - 100 baz.save - visit project_issues_path(project, sort: 'last_updated') + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_updated) - first_issue.should include("baz") + expect(first_issue).to include('baz') end describe 'sorting by milestone' do @@ -168,15 +178,15 @@ describe "Issues", feature: true do end it 'sorts by recently due milestone' do - visit project_issues_path(project, sort: 'milestone_due_soon') + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_milestone_soon) - first_issue.should include("foo") + expect(first_issue).to include('foo') end it 'sorts by least recently due milestone' do - visit project_issues_path(project, sort: 'milestone_due_later') + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_milestone_later) - first_issue.should include("bar") + expect(first_issue).to include('bar') end end @@ -191,11 +201,13 @@ describe "Issues", feature: true do end it 'sorts with a filter applied' do - visit project_issues_path(project, sort: 'oldest', assignee_id: user2.id) + visit namespace_project_issues_path(project.namespace, project, + sort: sort_value_oldest_created, + assignee_id: user2.id) - first_issue.should include("bar") - last_issue.should include("foo") - page.should_not have_content 'baz' + expect(first_issue).to include('bar') + expect(last_issue).to include('foo') + expect(page).not_to have_content 'baz' end end end @@ -206,13 +218,15 @@ describe "Issues", feature: true do context 'by autorized user' do it 'with dropdown menu' do - visit project_issue_path(project, issue) + visit namespace_project_issue_path(project.namespace, project, issue) - find('.edit-issue.inline-update #issue_assignee_id').set project.team.members.first.id + find('.edit-issue.inline-update #issue_assignee_id'). + set project.team.members.first.id click_button 'Update Issue' - page.should have_content "Assignee:" - has_select?('issue_assignee_id', :selected => project.team.members.first.name) + expect(page).to have_content 'Assignee:' + has_select?('issue_assignee_id', + selected: project.team.members.first.name) end end @@ -226,12 +240,12 @@ describe "Issues", feature: true do issue.save end - it "shows assignee text", js: true do + it 'shows assignee text', js: true do logout login_with guest - visit project_issue_path(project, issue) - page.should have_content issue.assignee.name + visit namespace_project_issue_path(project.namespace, project, issue) + expect(page).to have_content issue.assignee.name end end end @@ -243,13 +257,15 @@ describe "Issues", feature: true do context 'by authorized user' do it 'with dropdown menu' do - visit project_issue_path(project, issue) + visit namespace_project_issue_path(project.namespace, project, issue) - find('.edit-issue.inline-update').select(milestone.title, from: 'issue_milestone_id') + find('.edit-issue.inline-update'). + select(milestone.title, from: 'issue_milestone_id') click_button 'Update Issue' - page.should have_content "Milestone changed to #{milestone.title}" - has_select?('issue_assignee_id', :selected => milestone.title) + expect(page).to have_content "Milestone changed to #{milestone.title}" + expect(page).to have_content "Milestone: #{milestone.title}" + has_select?('issue_assignee_id', selected: milestone.title) end end @@ -262,12 +278,12 @@ describe "Issues", feature: true do issue.save end - it "shows milestone text", js: true do + it 'shows milestone text', js: true do logout login_with guest - visit project_issue_path(project, issue) - page.should have_content milestone.title + visit namespace_project_issue_path(project.namespace, project, issue) + expect(page).to have_content milestone.title end end @@ -280,25 +296,25 @@ describe "Issues", feature: true do end it 'allows user to remove assignee', :js => true do - visit project_issue_path(project, issue) - page.should have_content "Assignee: #{user2.name}" + visit namespace_project_issue_path(project.namespace, project, issue) + expect(page).to have_content "Assignee: #{user2.name}" first('#s2id_issue_assignee_id').click sleep 2 # wait for ajax stuff to complete first('.user-result').click - page.should have_content "Assignee: Unassigned" + expect(page).to have_content 'Assignee: none' sleep 2 # wait for ajax stuff to complete - issue.reload.assignee.should be_nil + expect(issue.reload.assignee).to be_nil end end end def first_issue - all("ul.issues-list li").first.text + page.all('ul.issues-list li').first.text end def last_issue - all("ul.issues-list li").last.text + page.all('ul.issues-list li').last.text end end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb new file mode 100644 index 00000000000..046a9f6191d --- /dev/null +++ b/spec/features/login_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +feature 'Login' do + describe 'with two-factor authentication' do + context 'with valid username/password' do + let(:user) { create(:user, :two_factor) } + + before do + login_with(user) + expect(page).to have_content('Two-factor Authentication') + end + + def enter_code(code) + fill_in 'Two-factor authentication code', with: code + click_button 'Verify code' + end + + it 'does not show a "You are already signed in." error message' do + enter_code(user.current_otp) + expect(page).not_to have_content('You are already signed in.') + end + + context 'using one-time code' do + it 'allows login with valid code' do + enter_code(user.current_otp) + expect(current_path).to eq root_path + end + + it 'blocks login with invalid code' do + enter_code('foo') + expect(page).to have_content('Invalid two-factor code') + end + + it 'allows login with invalid code, then valid code' do + enter_code('foo') + expect(page).to have_content('Invalid two-factor code') + + enter_code(user.current_otp) + expect(current_path).to eq root_path + end + end + + context 'using backup code' do + let(:codes) { user.generate_otp_backup_codes! } + + before do + expect(codes.size).to eq 10 + + # Ensure the generated codes get saved + user.save + end + + context 'with valid code' do + it 'allows login' do + enter_code(codes.sample) + expect(current_path).to eq root_path + end + + it 'invalidates the used code' do + expect { enter_code(codes.sample) }. + to change { user.reload.otp_backup_codes.size }.by(-1) + end + end + + context 'with invalid code' do + it 'blocks login' do + code = codes.sample + expect(user.invalidate_otp_backup_code!(code)).to eq true + + user.save! + expect(user.reload.otp_backup_codes.size).to eq 9 + + enter_code(code) + expect(page).to have_content('Invalid two-factor code.') + end + end + end + end + end + + describe 'without two-factor authentication' do + let(:user) { create(:user) } + + it 'allows basic login' do + login_with(user) + expect(current_path).to eq root_path + end + + it 'does not show a "You are already signed in." error message' do + login_with(user) + expect(page).not_to have_content('You are already signed in.') + end + + it 'blocks invalid login' do + user = create(:user, password: 'not-the-default') + + login_with(user) + expect(page).to have_content('Invalid email or password.') + end + end +end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb new file mode 100644 index 00000000000..902968cebcb --- /dev/null +++ b/spec/features/markdown_spec.rb @@ -0,0 +1,426 @@ +require 'spec_helper' +require 'erb' + +# This feature spec is intended to be a comprehensive exercising of all of +# GitLab's non-standard Markdown parsing and the integration thereof. +# +# These tests should be very high-level. Anything low-level belongs in the specs +# for the corresponding HTML::Pipeline filter or helper method. +# +# The idea is to pass a Markdown document through our entire processing stack. +# +# The process looks like this: +# +# Raw Markdown +# -> `markdown` helper +# -> Redcarpet::Render::GitlabHTML converts Markdown to HTML +# -> Post-process HTML +# -> `gfm_with_options` helper +# -> HTML::Pipeline +# -> Sanitize +# -> RelativeLink +# -> Emoji +# -> Table of Contents +# -> Autolinks +# -> Rinku (http, https, ftp) +# -> Other schemes +# -> ExternalLink +# -> References +# -> TaskList +# -> `html_safe` +# -> Template +# +# See the MarkdownFeature class for setup details. + +describe 'GitLab Markdown' do + include ActionView::Helpers::TagHelper + include ActionView::Helpers::UrlHelper + include Capybara::Node::Matchers + include GitlabMarkdownHelper + + # `markdown` calls these two methods + def current_user + @feat.user + end + + def user_color_scheme_class + :white + end + + # Let's only parse this thing once + before(:all) do + @feat = MarkdownFeature.new + + # `markdown` expects a `@project` variable + @project = @feat.project + + @md = markdown(@feat.raw_markdown) + @doc = Nokogiri::HTML::DocumentFragment.parse(@md) + end + + after(:all) do + @feat.teardown + end + + # Given a header ID, goes to that element's parent (the header itself), then + # its next sibling element (the body). + def get_section(id) + @doc.at_css("##{id}").parent.next_element + end + + # Sometimes it can be useful to see the parsed output of the Markdown document + # for debugging. Uncomment this block to write the output to + # tmp/capybara/markdown_spec.html. + # + # it 'writes to a file' do + # File.open(Rails.root.join('tmp/capybara/markdown_spec.html'), 'w') do |file| + # file.puts @md + # end + # end + + describe 'Markdown' do + describe 'No Intra Emphasis' do + it 'does not parse emphasis inside of words' do + body = get_section('no-intra-emphasis') + expect(body.to_html).not_to match('foo<em>bar</em>baz') + end + end + + describe 'Tables' do + it 'parses table Markdown' do + body = get_section('tables') + expect(body).to have_selector('th:contains("Header")') + expect(body).to have_selector('th:contains("Row")') + expect(body).to have_selector('th:contains("Example")') + end + + it 'allows Markdown in tables' do + expect(@doc.at_css('td:contains("Baz")').children.to_html). + to eq '<strong>Baz</strong>' + end + end + + describe 'Fenced Code Blocks' do + it 'parses fenced code blocks' do + expect(@doc).to have_selector('pre.code.highlight.white.c') + expect(@doc).to have_selector('pre.code.highlight.white.python') + end + end + + describe 'Strikethrough' do + it 'parses strikethroughs' do + expect(@doc).to have_selector(%{del:contains("and this text doesn't")}) + end + end + + describe 'Superscript' do + it 'parses superscript' do + body = get_section('superscript') + expect(body.to_html).to match('1<sup>st</sup>') + expect(body.to_html).to match('2<sup>nd</sup>') + end + end + end + + describe 'HTML::Pipeline' do + describe 'SanitizationFilter' do + it 'uses a permissive whitelist' do + expect(@doc).to have_selector('b:contains("b tag")') + expect(@doc).to have_selector('em:contains("em tag")') + expect(@doc).to have_selector('code:contains("code tag")') + expect(@doc).to have_selector('kbd:contains("s")') + expect(@doc).to have_selector('strike:contains(Emoji)') + expect(@doc).to have_selector('img[src*="smile.png"]') + expect(@doc).to have_selector('br') + expect(@doc).to have_selector('hr') + end + + it 'permits span elements' do + expect(@doc).to have_selector('span:contains("span tag")') + end + + it 'permits table alignment' do + expect(@doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center' + expect(@doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right' + expect(@doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left' + + expect(@doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center' + expect(@doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right' + expect(@doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left' + end + + it 'removes `rel` attribute from links' do + body = get_section('sanitizationfilter') + expect(body).not_to have_selector('a[rel="bookmark"]') + end + + it "removes `href` from `a` elements if it's fishy" do + expect(@doc).not_to have_selector('a[href*="javascript"]') + end + end + + describe 'Escaping' do + let(:table) { @doc.css('table').last.at_css('tbody') } + + it 'escapes non-tag angle brackets' do + expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 < 3 & 5' + end + end + + describe 'Edge Cases' do + it 'allows markup inside link elements' do + expect(@doc.at_css('a[href="#link-emphasis"]').to_html). + to eq %{<a href="#link-emphasis"><em>text</em></a>} + + expect(@doc.at_css('a[href="#link-strong"]').to_html). + to eq %{<a href="#link-strong"><strong>text</strong></a>} + + expect(@doc.at_css('a[href="#link-code"]').to_html). + to eq %{<a href="#link-code"><code>text</code></a>} + end + end + + describe 'EmojiFilter' do + it 'parses Emoji' do + expect(@doc).to have_selector('img.emoji', count: 10) + end + end + + describe 'TableOfContentsFilter' do + it 'creates anchors inside header elements' do + expect(@doc).to have_selector('h1 a#gitlab-markdown') + expect(@doc).to have_selector('h2 a#markdown') + expect(@doc).to have_selector('h3 a#autolinkfilter') + end + end + + describe 'AutolinkFilter' do + let(:list) { get_section('autolinkfilter').next_element } + + def item(index) + list.at_css("li:nth-child(#{index})") + end + + it 'autolinks http://' do + expect(item(1).children.first.name).to eq 'a' + expect(item(1).children.first['href']).to eq 'http://about.gitlab.com/' + end + + it 'autolinks https://' do + expect(item(2).children.first.name).to eq 'a' + expect(item(2).children.first['href']).to eq 'https://google.com/' + end + + it 'autolinks ftp://' do + expect(item(3).children.first.name).to eq 'a' + expect(item(3).children.first['href']).to eq 'ftp://ftp.us.debian.org/debian/' + end + + it 'autolinks smb://' do + expect(item(4).children.first.name).to eq 'a' + expect(item(4).children.first['href']).to eq 'smb://foo/bar/baz' + end + + it 'autolinks irc://' do + expect(item(5).children.first.name).to eq 'a' + expect(item(5).children.first['href']).to eq 'irc://irc.freenode.net/git' + end + + it 'autolinks short, invalid URLs' do + expect(item(6).children.first.name).to eq 'a' + expect(item(6).children.first['href']).to eq 'http://localhost:3000' + end + + %w(code a kbd).each do |elem| + it "ignores links inside '#{elem}' element" do + body = get_section('autolinkfilter') + expect(body).not_to have_selector("#{elem} a") + end + end + end + + describe 'ExternalLinkFilter' do + let(:links) { get_section('externallinkfilter').next_element } + + it 'adds nofollow to external link' do + expect(links.css('a').first.to_html).to match 'nofollow' + end + + it 'ignores internal link' do + expect(links.css('a').last.to_html).not_to match 'nofollow' + end + end + + describe 'ReferenceFilter' do + it 'handles references in headers' do + header = @doc.at_css('#reference-filters-eg-1').parent + + expect(header.css('a').size).to eq 2 + end + + it "handles references in Markdown" do + body = get_section('reference-filters-eg-1') + expect(body).to have_selector('em a.gfm-merge_request', count: 1) + end + + it 'parses user references' do + body = get_section('userreferencefilter') + expect(body).to have_selector('a.gfm.gfm-project_member', count: 3) + end + + it 'parses issue references' do + body = get_section('issuereferencefilter') + expect(body).to have_selector('a.gfm.gfm-issue', count: 2) + end + + it 'parses merge request references' do + body = get_section('mergerequestreferencefilter') + expect(body).to have_selector('a.gfm.gfm-merge_request', count: 2) + end + + it 'parses snippet references' do + body = get_section('snippetreferencefilter') + expect(body).to have_selector('a.gfm.gfm-snippet', count: 2) + end + + it 'parses commit range references' do + body = get_section('commitrangereferencefilter') + expect(body).to have_selector('a.gfm.gfm-commit_range', count: 2) + end + + it 'parses commit references' do + body = get_section('commitreferencefilter') + expect(body).to have_selector('a.gfm.gfm-commit', count: 2) + end + + it 'parses label references' do + body = get_section('labelreferencefilter') + expect(body).to have_selector('a.gfm.gfm-label', count: 3) + end + end + + describe 'Task Lists' do + it 'generates task lists' do + body = get_section('task-lists') + expect(body).to have_selector('ul.task-list', count: 2) + expect(body).to have_selector('li.task-list-item', count: 7) + expect(body).to have_selector('input[checked]', count: 3) + end + end + end +end + +# This is a helper class used by the GitLab Markdown feature spec +# +# Because the feature spec only cares about the output of the Markdown, and the +# test setup and teardown and parsing is fairly expensive, we only want to do it +# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)` +# block, so we fake it by encapsulating all the shared setup in this class. +# +# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for +# reference to the factory-created objects. +class MarkdownFeature + include FactoryGirl::Syntax::Methods + + def initialize + DatabaseCleaner.start + end + + def teardown + DatabaseCleaner.clean + end + + def user + @user ||= create(:user) + end + + def group + unless @group + @group = create(:group) + @group.add_user(user, Gitlab::Access::DEVELOPER) + end + + @group + end + + # Direct references ---------------------------------------------------------- + + def project + @project ||= create(:project) + end + + def issue + @issue ||= create(:issue, project: project) + end + + def merge_request + @merge_request ||= create(:merge_request, :simple, source_project: project) + end + + def snippet + @snippet ||= create(:project_snippet, project: project) + end + + def commit + @commit ||= project.commit + end + + def commit_range + unless @commit_range + commit2 = project.commit('HEAD~3') + @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project) + end + + @commit_range + end + + def simple_label + @simple_label ||= create(:label, name: 'gfm', project: project) + end + + def label + @label ||= create(:label, name: 'awaiting feedback', project: project) + end + + # Cross-references ----------------------------------------------------------- + + def xproject + unless @xproject + namespace = create(:namespace, name: 'cross-reference') + @xproject = create(:project, namespace: namespace) + @xproject.team << [user, :developer] + end + + @xproject + end + + def xissue + @xissue ||= create(:issue, project: xproject) + end + + def xmerge_request + @xmerge_request ||= create(:merge_request, :simple, source_project: xproject) + end + + def xsnippet + @xsnippet ||= create(:project_snippet, project: xproject) + end + + def xcommit + @xcommit ||= xproject.commit + end + + def xcommit_range + unless @xcommit_range + xcommit2 = xproject.commit('HEAD~2') + @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) + end + + @xcommit_range + end + + def raw_markdown + fixture = Rails.root.join('spec/fixtures/markdown.md.erb') + ERB.new(File.read(fixture)).result(binding) + end +end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 92f3a6c0929..219bb3129e7 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -3,197 +3,213 @@ require 'spec_helper' describe 'Comments' do include RepoHelpers - describe "On a merge request", js: true, feature: true do + describe 'On a merge request', js: true, feature: true do let!(:merge_request) { create(:merge_request) } let!(:project) { merge_request.source_project } - let!(:note) { create(:note_on_merge_request, :with_attachment, project: project) } + let!(:note) do + create(:note_on_merge_request, :with_attachment, project: project) + end before do login_as :admin - visit project_merge_request_path(project, merge_request) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) end subject { page } - describe "the note form" do + describe 'the note form' do it 'should be valid' do - should have_css(".js-main-target-form", visible: true, count: 1) - find(".js-main-target-form input[type=submit]").value.should == "Add Comment" - within(".js-main-target-form") { should_not have_link("Cancel") } - within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } + is_expected.to have_css('.js-main-target-form', visible: true, count: 1) + expect(find('.js-main-target-form input[type=submit]').value). + to eq('Add Comment') + page.within('.js-main-target-form') do + expect(page).not_to have_link('Cancel') + end end - describe "with text" do + describe 'with text' do before do - within(".js-main-target-form") do - fill_in "note[note]", with: "This is awesome" + page.within('.js-main-target-form') do + fill_in 'note[note]', with: 'This is awesome' end end it 'should have enable submit button and preview button' do - within(".js-main-target-form") { should_not have_css(".js-comment-button[disabled]") } - within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: true) } + page.within('.js-main-target-form') do + expect(page).not_to have_css('.js-comment-button[disabled]') + expect(page).to have_css('.js-md-preview-button', visible: true) + end end end end - describe "when posting a note" do + describe 'when posting a note' do before do - within(".js-main-target-form") do - fill_in "note[note]", with: "This is awsome!" - find(".js-note-preview-button").trigger("click") - click_button "Add Comment" + page.within('.js-main-target-form') do + fill_in 'note[note]', with: 'This is awsome!' + find('.js-md-preview-button').click + click_button 'Add Comment' end end it 'should be added and form reset' do - should have_content("This is awsome!") - within(".js-main-target-form") { should have_no_field("note[note]", with: "This is awesome!") } - within(".js-main-target-form") { should have_css(".js-note-preview", visible: false) } - within(".js-main-target-form") { should have_css(".js-note-text", visible: true) } + is_expected.to have_content('This is awsome!') + page.within('.js-main-target-form') do + expect(page).to have_no_field('note[note]', with: 'This is awesome!') + expect(page).to have_css('.js-md-preview', visible: :hidden) + end + page.within('.js-main-target-form') do + is_expected.to have_css('.js-note-text', visible: true) + end end end - describe "when editing a note", js: true do - it "should contain the hidden edit form" do - within("#note_#{note.id}") { should have_css(".note-edit-form", visible: false) } + describe 'when editing a note', js: true do + it 'should contain the hidden edit form' do + page.within("#note_#{note.id}") do + is_expected.to have_css('.note-edit-form', visible: false) + end end - describe "editing the note" do + describe 'editing the note' do before do find('.note').hover find(".js-note-edit").click end - it "should show the note edit form and hide the note body" do - within("#note_#{note.id}") do - find(".note-edit-form", visible: true).should be_visible - find(".note-text", visible: false).should_not be_visible - end - end - - it "should reset the edit note form textarea with the original content of the note if cancelled" do - find('.note').hover - find(".js-note-edit").click - - within(".note-edit-form") do - fill_in "note[note]", with: "Some new content" - find(".btn-cancel").click - find(".js-note-text", visible: false).text.should == note.note + it 'should show the note edit form and hide the note body' do + page.within("#note_#{note.id}") do + expect(find('.current-note-edit-form', visible: true)).to be_visible + expect(find('.note-edit-form', visible: true)).to be_visible + expect(find(:css, '.note-body > .note-text', visible: false)).not_to be_visible end end - it "appends the edited at time to the note" do - find('.note').hover - find(".js-note-edit").click - - within(".note-edit-form") do - fill_in "note[note]", with: "Some new content" - find(".btn-save").click + # TODO: fix after 7.7 release + #it "should reset the edit note form textarea with the original content of the note if cancelled" do + #within(".current-note-edit-form") do + #fill_in "note[note]", with: "Some new content" + #find(".btn-cancel").click + #expect(find(".js-note-text", visible: false).text).to eq note.note + #end + #end + + it 'appends the edited at time to the note' do + page.within('.current-note-edit-form') do + fill_in 'note[note]', with: 'Some new content' + find('.btn-save').click end - within("#note_#{note.id}") do - should have_css(".note-last-update small") - find(".note-last-update small").text.should match(/Edited less than a minute ago/) + page.within("#note_#{note.id}") do + is_expected.to have_css('.note_edited_ago') + expect(find('.note_edited_ago').text). + to match(/less than a minute ago/) end end end - describe "deleting an attachment" do + describe 'deleting an attachment' do before do find('.note').hover - find(".js-note-edit").click + find('.js-note-edit').click end - it "shows the delete link" do - within(".note-attachment") do - should have_css(".js-note-attachment-delete") + it 'shows the delete link' do + page.within('.note-attachment') do + is_expected.to have_css('.js-note-attachment-delete') end end - it "removes the attachment div and resets the edit form" do - find(".js-note-attachment-delete").click - should_not have_css(".note-attachment") - find(".note-edit-form", visible: false).should_not be_visible + it 'removes the attachment div and resets the edit form' do + find('.js-note-attachment-delete').click + is_expected.not_to have_css('.note-attachment') + expect(find('.current-note-edit-form', visible: false)). + not_to be_visible end end end end - describe "On a merge request diff", js: true, feature: true do + describe 'On a merge request diff', js: true, feature: true do let(:merge_request) { create(:merge_request) } let(:project) { merge_request.source_project } before do login_as :admin - visit diffs_project_merge_request_path(project, merge_request) + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) end subject { page } - describe "when adding a note" do + describe 'when adding a note' do before do click_diff_line end - describe "the notes holder" do - it { should have_css(".js-temp-notes-holder") } + describe 'the notes holder' do + it { is_expected.to have_css('.js-temp-notes-holder') } - it { within(".js-temp-notes-holder") { should have_css(".new_note") } } + it 'has .new_note css class' do + page.within('.js-temp-notes-holder') do + expect(subject).to have_css('.new_note') + end + end end - describe "the note form" do + describe 'the note form' do it "shouldn't add a second form for same row" do click_diff_line - should have_css("tr[id='#{line_code}'] + .js-temp-notes-holder form", count: 1) + is_expected. + to have_css("tr[id='#{line_code}'] + .js-temp-notes-holder form", + count: 1) end - it "should be removed when canceled" do - within(".diff-file form[rel$='#{line_code}']") do - find(".js-close-discussion-note-form").trigger("click") + it 'should be removed when canceled' do + page.within(".diff-file form[rel$='#{line_code}']") do + find('.js-close-discussion-note-form').trigger('click') end - should have_no_css(".js-temp-notes-holder") + is_expected.to have_no_css('.js-temp-notes-holder') end end end - describe "with muliple note forms" do + describe 'with muliple note forms' do before do click_diff_line click_diff_line(line_code_2) end - it { should have_css(".js-temp-notes-holder", count: 2) } + it { is_expected.to have_css('.js-temp-notes-holder', count: 2) } - describe "previewing them separately" do + describe 'previewing them separately' do before do # add two separate texts and trigger previews on both - within("tr[id='#{line_code}'] + .js-temp-notes-holder") do - fill_in "note[note]", with: "One comment on line 7" - find(".js-note-preview-button").trigger("click") + page.within("tr[id='#{line_code}'] + .js-temp-notes-holder") do + fill_in 'note[note]', with: 'One comment on line 7' + find('.js-md-preview-button').click end - within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do - fill_in "note[note]", with: "Another comment on line 10" - find(".js-note-preview-button").trigger("click") + page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do + fill_in 'note[note]', with: 'Another comment on line 10' + find('.js-md-preview-button').click end end end - describe "posting a note" do + describe 'posting a note' do before do - within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do - fill_in "note[note]", with: "Another comment on line 10" - click_button("Add Comment") + page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do + fill_in 'note[note]', with: 'Another comment on line 10' + click_button('Add Comment') end end it 'should be added as discussion' do - should have_content("Another comment on line 10") - should have_css(".notes_holder") - should have_css(".notes_holder .note", count: 1) - should have_button('Reply') + is_expected.to have_content('Another comment on line 10') + is_expected.to have_css('.notes_holder') + is_expected.to have_css('.notes_holder .note', count: 1) + is_expected.to have_button('Reply') end end end diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb new file mode 100644 index 00000000000..a34efce09ef --- /dev/null +++ b/spec/features/password_reset_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +feature 'Password reset' do + def forgot_password + click_on 'Forgot your password?' + fill_in 'Email', with: user.email + click_button 'Reset password' + user.reload + end + + def get_reset_token + mail = ActionMailer::Base.deliveries.last + body = mail.body.encoded + body.scan(/reset_password_token=(.+)\"/).flatten.first + end + + def reset_password(password = 'password') + visit edit_user_password_path(reset_password_token: get_reset_token) + + fill_in 'New password', with: password + fill_in 'Confirm new password', with: password + click_button 'Change your password' + end + + describe 'with two-factor authentication' do + let(:user) { create(:user, :two_factor) } + + it 'requires login after password reset' do + visit root_path + + forgot_password + reset_password + + expect(page).to have_content("Your password was changed successfully.") + expect(page).not_to have_content("You are now signed in.") + expect(current_path).to eq new_user_session_path + end + end + + describe 'without two-factor authentication' do + let(:user) { create(:user) } + + it 'automatically logs in after password reset' do + visit root_path + + forgot_password + reset_password + + expect(current_path).to eq root_path + expect(page).to have_content("Your password was changed successfully. You are now signed in.") + end + end +end diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index bdf7b59114b..9fe2e610555 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -1,35 +1,37 @@ require 'spec_helper' -describe "Profile account page", feature: true do +describe 'Profile account page', feature: true do let(:user) { create(:user) } before do login_as :user end - describe "when signup is enabled" do + describe 'when signup is enabled' do before do - Gitlab.config.gitlab.stub(:signup_enabled).and_return(true) + allow_any_instance_of(ApplicationSetting). + to receive(:signup_enabled?).and_return(true) visit profile_account_path end - it { page.should have_content("Remove account") } + it { expect(page).to have_content('Remove account') } - it "should delete the account" do - expect { click_link "Delete account" }.to change {User.count}.by(-1) - current_path.should == new_user_session_path + it 'should delete the account' do + expect { click_link 'Delete account' }.to change { User.count }.by(-1) + expect(current_path).to eq(new_user_session_path) end end - describe "when signup is disabled" do + describe 'when signup is disabled' do before do - Gitlab.config.gitlab.stub(:signup_enabled).and_return(false) + allow_any_instance_of(ApplicationSetting). + to receive(:signup_enabled?).and_return(false) visit profile_account_path end - it "should not have option to remove account" do - page.should_not have_content("Remove account") - current_path.should == profile_account_path + it 'should not have option to remove account' do + expect(page).not_to have_content('Remove account') + expect(current_path).to eq(profile_account_path) end end end diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb new file mode 100644 index 00000000000..03e78c533db --- /dev/null +++ b/spec/features/profiles/preferences_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe 'Profile > Preferences' do + let(:user) { create(:user) } + + before do + login_as(user) + visit profile_preferences_path + end + + describe 'User changes their application theme', js: true do + let(:default) { Gitlab::Themes.default } + let(:theme) { Gitlab::Themes.by_id(5) } + + it 'creates a flash message' do + choose "user_theme_id_#{theme.id}" + + expect_preferences_saved_message + end + + it 'updates their preference' do + choose "user_theme_id_#{theme.id}" + + allowing_for_delay do + visit page.current_path + expect(page).to have_checked_field("user_theme_id_#{theme.id}") + end + end + + it 'reflects the changes immediately' do + expect(page).to have_selector("body.#{default.css_class}") + + choose "user_theme_id_#{theme.id}" + + expect(page).not_to have_selector("body.#{default.css_class}") + expect(page).to have_selector("body.#{theme.css_class}") + end + end + + describe 'User changes their syntax highlighting theme', js: true do + it 'creates a flash message' do + choose 'user_color_scheme_id_5' + + expect_preferences_saved_message + end + + it 'updates their preference' do + choose 'user_color_scheme_id_5' + + allowing_for_delay do + visit page.current_path + expect(page).to have_checked_field('user_color_scheme_id_5') + end + end + end + + describe 'User changes their default dashboard' do + it 'creates a flash message' do + select 'Starred Projects', from: 'user_dashboard' + click_button 'Save' + + expect_preferences_saved_message + end + + it 'updates their preference' do + select 'Starred Projects', from: 'user_dashboard' + click_button 'Save' + + click_link 'Dashboard' + expect(page.current_path).to eq starred_dashboard_projects_path + + click_link 'Your Projects' + expect(page.current_path).to eq dashboard_path + end + end + + def expect_preferences_saved_message + page.within('.flash-container') do + expect(page).to have_content('Preferences saved.') + end + end +end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index d291621935b..f8eea70ec4a 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -1,32 +1,57 @@ require 'spec_helper' -describe "Projects", feature: true, js: true do - before { login_as :user } +feature 'Project' do + describe 'description' do + let(:project) { create(:project) } + let(:path) { namespace_project_path(project.namespace, project) } - describe "DELETE /projects/:id" do before do - @project = create(:project, namespace: @user.namespace) - @project.team << [@user, :master] - visit edit_project_path(@project) + login_as(:admin) end - it "should remove project" do - expect { remove_project }.to change {Project.count}.by(-1) + it 'parses Markdown' do + project.update_attribute(:description, 'This is **my** project') + visit path + expect(page).to have_css('.project-home-desc > p > strong') + end + + it 'passes through html-pipeline' do + project.update_attribute(:description, 'This project is the :poop:') + visit path + expect(page).to have_css('.project-home-desc > p > img') + end + + it 'sanitizes unwanted tags' do + project.update_attribute(:description, '# Project Description') + visit path + expect(page).not_to have_css('.project-home-desc h1') + end + + it 'permits `rel` attribute on links' do + project.update_attribute(:description, 'https://google.com/') + visit path + expect(page).to have_css('.project-home-desc a[rel]') end + end + + describe 'removal', js: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } - it 'should delete the project from disk' do - expect(GitlabShellWorker).to( - receive(:perform_async).with(:remove_repository, - /#{@project.path_with_namespace}/) - ).twice + before do + login_with(user) + project.team << [user, :master] + visit edit_namespace_project_path(project.namespace, project) + end - remove_project + it 'should remove project' do + expect { remove_project }.to change {Project.count}.by(-1) end end def remove_project click_link "Remove project" - fill_in 'confirm_name_input', with: @project.path + fill_in 'confirm_name_input', with: project.path click_button 'Confirm' end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index cce9f06cb69..479334f45d8 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -7,14 +7,14 @@ describe "Search", feature: true do @project.team << [@user, :reporter] visit search_path - within '.search-holder' do + page.within '.search-holder' do fill_in "search", with: @project.name[0..3] click_button "Search" end end it "should show project in search results" do - page.should have_content @project.name + expect(page).to have_content @project.name end end diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb index 1cca82cef64..67238e3ab76 100644 --- a/spec/features/security/dashboard_access_spec.rb +++ b/spec/features/security/dashboard_access_spec.rb @@ -4,52 +4,60 @@ describe "Dashboard access", feature: true do describe "GET /dashboard" do subject { dashboard_path } - it { should be_allowed_for :admin } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /dashboard/issues" do subject { issues_dashboard_path } - it { should be_allowed_for :admin } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /dashboard/merge_requests" do subject { merge_requests_dashboard_path } - it { should be_allowed_for :admin } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end - describe "GET /dashboard/projects" do - subject { projects_dashboard_path } + describe "GET /dashboard/projects/starred" do + subject { starred_dashboard_projects_path } - it { should be_allowed_for :admin } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /help" do subject { help_path } - it { should be_allowed_for :admin } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /projects/new" do - it { new_project_path.should be_allowed_for :admin } - it { new_project_path.should be_allowed_for :user } - it { new_project_path.should be_denied_for :visitor } + it { expect(new_project_path).to be_allowed_for :admin } + it { expect(new_project_path).to be_allowed_for :user } + it { expect(new_project_path).to be_denied_for :visitor } end describe "GET /groups/new" do - it { new_group_path.should be_allowed_for :admin } - it { new_group_path.should be_allowed_for :user } - it { new_group_path.should be_denied_for :visitor } + it { expect(new_group_path).to be_allowed_for :admin } + it { expect(new_group_path).to be_allowed_for :user } + it { expect(new_group_path).to be_denied_for :visitor } + end + + describe "GET /profile/groups" do + subject { dashboard_groups_path } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end end diff --git a/spec/features/security/group/group_access_spec.rb b/spec/features/security/group/group_access_spec.rb index 44de499e6d2..63793149459 100644 --- a/spec/features/security/group/group_access_spec.rb +++ b/spec/features/security/group/group_access_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe "Group access", feature: true do describe "GET /projects/new" do - it { new_group_path.should be_allowed_for :admin } - it { new_group_path.should be_allowed_for :user } - it { new_group_path.should be_denied_for :visitor } + it { expect(new_group_path).to be_allowed_for :admin } + it { expect(new_group_path).to be_allowed_for :user } + it { expect(new_group_path).to be_denied_for :visitor } end describe "Group" do @@ -26,73 +26,73 @@ describe "Group access", feature: true do describe "GET /groups/:path" do subject { group_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /groups/:path/issues" do subject { issues_group_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /groups/:path/merge_requests" do subject { merge_requests_group_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end - describe "GET /groups/:path/members" do - subject { members_group_path(group) } + describe "GET /groups/:path/group_members" do + subject { group_group_members_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /groups/:path/edit" do subject { edit_group_path(group) } - it { should be_allowed_for owner } - it { should be_denied_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_denied_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /groups/:path/projects" do subject { projects_group_path(group) } - it { should be_allowed_for owner } - it { should be_denied_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_denied_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end end end diff --git a/spec/features/security/group/internal_group_access_spec.rb b/spec/features/security/group/internal_group_access_spec.rb index da5c6eb4e91..d17a7412e43 100644 --- a/spec/features/security/group/internal_group_access_spec.rb +++ b/spec/features/security/group/internal_group_access_spec.rb @@ -22,61 +22,61 @@ describe "Group with internal project access", feature: true do describe "GET /groups/:path" do subject { group_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /groups/:path/issues" do subject { issues_group_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /groups/:path/merge_requests" do subject { merge_requests_group_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end - describe "GET /groups/:path/members" do - subject { members_group_path(group) } + describe "GET /groups/:path/group_members" do + subject { group_group_members_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /groups/:path/edit" do subject { edit_group_path(group) } - it { should be_allowed_for owner } - it { should be_denied_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_denied_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end end end diff --git a/spec/features/security/group/mixed_group_access_spec.rb b/spec/features/security/group/mixed_group_access_spec.rb index c9889d99590..b3db7b5dea4 100644 --- a/spec/features/security/group/mixed_group_access_spec.rb +++ b/spec/features/security/group/mixed_group_access_spec.rb @@ -23,61 +23,61 @@ describe "Group access", feature: true do describe "GET /groups/:path" do subject { group_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /groups/:path/issues" do subject { issues_group_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /groups/:path/merge_requests" do subject { merge_requests_group_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end - describe "GET /groups/:path/members" do - subject { members_group_path(group) } + describe "GET /groups/:path/group_members" do + subject { group_group_members_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /groups/:path/edit" do subject { edit_group_path(group) } - it { should be_allowed_for owner } - it { should be_denied_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_denied_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end end end diff --git a/spec/features/security/group/public_group_access_spec.rb b/spec/features/security/group/public_group_access_spec.rb index 2e76ab154ff..c16f0c0d1e1 100644 --- a/spec/features/security/group/public_group_access_spec.rb +++ b/spec/features/security/group/public_group_access_spec.rb @@ -22,61 +22,61 @@ describe "Group with public project access", feature: true do describe "GET /groups/:path" do subject { group_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /groups/:path/issues" do subject { issues_group_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /groups/:path/merge_requests" do subject { merge_requests_group_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end - describe "GET /groups/:path/members" do - subject { members_group_path(group) } + describe "GET /groups/:path/group_members" do + subject { group_group_members_path(group) } - it { should be_allowed_for owner } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /groups/:path/edit" do subject { edit_group_path(group) } - it { should be_allowed_for owner } - it { should be_denied_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_denied_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end end end diff --git a/spec/features/security/profile_access_spec.rb b/spec/features/security/profile_access_spec.rb index 4efc0ffdcd3..8f7a9606262 100644 --- a/spec/features/security/profile_access_spec.rb +++ b/spec/features/security/profile_access_spec.rb @@ -1,76 +1,65 @@ require 'spec_helper' -describe "Users Security", feature: true do - describe "Project" do - before do - @u1 = create(:user) - end - - describe "GET /login" do - it { new_user_session_path.should_not be_404_for :visitor } - end - - describe "GET /profile/keys" do - subject { profile_keys_path } +describe "Profile access", feature: true do + before do + @u1 = create(:user) + end - it { should be_allowed_for @u1 } - it { should be_allowed_for :admin } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } - end + describe "GET /login" do + it { expect(new_user_session_path).not_to be_not_found_for :visitor } + end - describe "GET /profile" do - subject { profile_path } + describe "GET /profile/keys" do + subject { profile_keys_path } - it { should be_allowed_for @u1 } - it { should be_allowed_for :admin } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } - end + it { is_expected.to be_allowed_for @u1 } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end - describe "GET /profile/account" do - subject { profile_account_path } + describe "GET /profile" do + subject { profile_path } - it { should be_allowed_for @u1 } - it { should be_allowed_for :admin } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } - end + it { is_expected.to be_allowed_for @u1 } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end - describe "GET /profile/design" do - subject { design_profile_path } + describe "GET /profile/account" do + subject { profile_account_path } - it { should be_allowed_for @u1 } - it { should be_allowed_for :admin } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } - end + it { is_expected.to be_allowed_for @u1 } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end - describe "GET /profile/history" do - subject { history_profile_path } + describe "GET /profile/preferences" do + subject { profile_preferences_path } - it { should be_allowed_for @u1 } - it { should be_allowed_for :admin } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } - end + it { is_expected.to be_allowed_for @u1 } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end - describe "GET /profile/notifications" do - subject { profile_notifications_path } + describe "GET /profile/history" do + subject { history_profile_path } - it { should be_allowed_for @u1 } - it { should be_allowed_for :admin } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } - end + it { is_expected.to be_allowed_for @u1 } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end - describe "GET /profile/groups" do - subject { profile_groups_path } + describe "GET /profile/notifications" do + subject { profile_notifications_path } - it { should be_allowed_for @u1 } - it { should be_allowed_for :admin } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } - end + it { is_expected.to be_allowed_for @u1 } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 598d554a946..8d1bfd25223 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -18,207 +18,210 @@ describe "Internal Project Access", feature: true do describe "Project should be internal" do subject { project } - its(:internal?) { should be_true } + describe '#internal?' do + subject { super().internal? } + it { is_expected.to be_truthy } + end end describe "GET /:project_path" do - subject { project_path(project) } + subject { namespace_project_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/tree/master" do - subject { project_tree_path(project, project.repository.root_ref) } + subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/commits/master" do - subject { project_commits_path(project, project.repository.root_ref, limit: 1) } + subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/commit/:sha" do - subject { project_commit_path(project, project.repository.commit) } + subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/compare" do - subject { project_compare_index_path(project) } + subject { namespace_project_compare_index_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end - describe "GET /:project_path/team" do - subject { project_team_index_path(project) } + describe "GET /:project_path/project_members" do + subject { namespace_project_project_members_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/blob" do before do commit = project.repository.commit path = '.gitignore' - @blob_path = project_blob_path(project, File.join(commit.id, path)) + @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path)) end - it { @blob_path.should be_allowed_for master } - it { @blob_path.should be_allowed_for reporter } - it { @blob_path.should be_allowed_for :admin } - it { @blob_path.should be_allowed_for guest } - it { @blob_path.should be_allowed_for :user } - it { @blob_path.should be_denied_for :visitor } + it { expect(@blob_path).to be_allowed_for master } + it { expect(@blob_path).to be_allowed_for reporter } + it { expect(@blob_path).to be_allowed_for :admin } + it { expect(@blob_path).to be_allowed_for guest } + it { expect(@blob_path).to be_allowed_for :user } + it { expect(@blob_path).to be_denied_for :visitor } end describe "GET /:project_path/edit" do - subject { edit_project_path(project) } + subject { edit_namespace_project_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/deploy_keys" do - subject { project_deploy_keys_path(project) } + subject { namespace_project_deploy_keys_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/issues" do - subject { project_issues_path(project) } + subject { namespace_project_issues_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/snippets" do - subject { project_snippets_path(project) } + subject { namespace_project_snippets_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/snippets/new" do - subject { new_project_snippet_path(project) } + subject { new_namespace_project_snippet_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/merge_requests" do - subject { project_merge_requests_path(project) } + subject { namespace_project_merge_requests_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/merge_requests/new" do - subject { new_project_merge_request_path(project) } + subject { new_namespace_project_merge_request_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/branches" do - subject { project_branches_path(project) } + subject { namespace_project_branches_path(project.namespace, project) } before do # Speed increase - Project.any_instance.stub(:branches).and_return([]) + allow_any_instance_of(Project).to receive(:branches).and_return([]) end - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/tags" do - subject { project_tags_path(project) } + subject { namespace_project_tags_path(project.namespace, project) } before do # Speed increase - Project.any_instance.stub(:tags).and_return([]) + allow_any_instance_of(Project).to receive(:tags).and_return([]) end - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/hooks" do - subject { project_hooks_path(project) } - - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + subject { namespace_project_hooks_path(project.namespace, project) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end end diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index b1d4c79e05b..9021ff33186 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -18,185 +18,188 @@ describe "Private Project Access", feature: true do describe "Project should be private" do subject { project } - its(:private?) { should be_true } + describe '#private?' do + subject { super().private? } + it { is_expected.to be_truthy } + end end describe "GET /:project_path" do - subject { project_path(project) } + subject { namespace_project_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/tree/master" do - subject { project_tree_path(project, project.repository.root_ref) } + subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/commits/master" do - subject { project_commits_path(project, project.repository.root_ref, limit: 1) } + subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/commit/:sha" do - subject { project_commit_path(project, project.repository.commit) } + subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/compare" do - subject { project_compare_index_path(project) } + subject { namespace_project_compare_index_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end - describe "GET /:project_path/team" do - subject { project_team_index_path(project) } + describe "GET /:project_path/project_members" do + subject { namespace_project_project_members_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/blob" do before do commit = project.repository.commit path = '.gitignore' - @blob_path = project_blob_path(project, File.join(commit.id, path)) + @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path)) end - it { @blob_path.should be_allowed_for master } - it { @blob_path.should be_allowed_for reporter } - it { @blob_path.should be_allowed_for :admin } - it { @blob_path.should be_denied_for guest } - it { @blob_path.should be_denied_for :user } - it { @blob_path.should be_denied_for :visitor } + it { expect(@blob_path).to be_allowed_for master } + it { expect(@blob_path).to be_allowed_for reporter } + it { expect(@blob_path).to be_allowed_for :admin } + it { expect(@blob_path).to be_denied_for guest } + it { expect(@blob_path).to be_denied_for :user } + it { expect(@blob_path).to be_denied_for :visitor } end describe "GET /:project_path/edit" do - subject { edit_project_path(project) } + subject { edit_namespace_project_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/deploy_keys" do - subject { project_deploy_keys_path(project) } + subject { namespace_project_deploy_keys_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/issues" do - subject { project_issues_path(project) } + subject { namespace_project_issues_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/snippets" do - subject { project_snippets_path(project) } + subject { namespace_project_snippets_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/merge_requests" do - subject { project_merge_requests_path(project) } + subject { namespace_project_merge_requests_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/branches" do - subject { project_branches_path(project) } + subject { namespace_project_branches_path(project.namespace, project) } before do # Speed increase - Project.any_instance.stub(:branches).and_return([]) + allow_any_instance_of(Project).to receive(:branches).and_return([]) end - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/tags" do - subject { project_tags_path(project) } + subject { namespace_project_tags_path(project.namespace, project) } before do # Speed increase - Project.any_instance.stub(:tags).and_return([]) + allow_any_instance_of(Project).to receive(:tags).and_return([]) end - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/hooks" do - subject { project_hooks_path(project) } - - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + subject { namespace_project_hooks_path(project.namespace, project) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end end diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index a4c8a2be25a..6ec190ed777 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -23,207 +23,210 @@ describe "Public Project Access", feature: true do describe "Project should be public" do subject { project } - its(:public?) { should be_true } + describe '#public?' do + subject { super().public? } + it { is_expected.to be_truthy } + end end describe "GET /:project_path" do - subject { project_path(project) } + subject { namespace_project_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /:project_path/tree/master" do - subject { project_tree_path(project, project.repository.root_ref) } + subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /:project_path/commits/master" do - subject { project_commits_path(project, project.repository.root_ref, limit: 1) } + subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /:project_path/commit/:sha" do - subject { project_commit_path(project, project.repository.commit) } + subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /:project_path/compare" do - subject { project_compare_index_path(project) } + subject { namespace_project_compare_index_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end - describe "GET /:project_path/team" do - subject { project_team_index_path(project) } + describe "GET /:project_path/project_members" do + subject { namespace_project_project_members_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/blob" do before do commit = project.repository.commit path = '.gitignore' - @blob_path = project_blob_path(project, File.join(commit.id, path)) + @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path)) end - it { @blob_path.should be_allowed_for master } - it { @blob_path.should be_allowed_for reporter } - it { @blob_path.should be_allowed_for :admin } - it { @blob_path.should be_allowed_for guest } - it { @blob_path.should be_allowed_for :user } - it { @blob_path.should be_allowed_for :visitor } + it { expect(@blob_path).to be_allowed_for master } + it { expect(@blob_path).to be_allowed_for reporter } + it { expect(@blob_path).to be_allowed_for :admin } + it { expect(@blob_path).to be_allowed_for guest } + it { expect(@blob_path).to be_allowed_for :user } + it { expect(@blob_path).to be_allowed_for :visitor } end describe "GET /:project_path/edit" do - subject { edit_project_path(project) } + subject { edit_namespace_project_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/deploy_keys" do - subject { project_deploy_keys_path(project) } + subject { namespace_project_deploy_keys_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/issues" do - subject { project_issues_path(project) } + subject { namespace_project_issues_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /:project_path/snippets" do - subject { project_snippets_path(project) } + subject { namespace_project_snippets_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /:project_path/snippets/new" do - subject { new_project_snippet_path(project) } + subject { new_namespace_project_snippet_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/merge_requests" do - subject { project_merge_requests_path(project) } + subject { namespace_project_merge_requests_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /:project_path/merge_requests/new" do - subject { new_project_merge_request_path(project) } + subject { new_namespace_project_merge_request_path(project.namespace, project) } - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/branches" do - subject { project_branches_path(project) } + subject { namespace_project_branches_path(project.namespace, project) } before do # Speed increase - Project.any_instance.stub(:branches).and_return([]) + allow_any_instance_of(Project).to receive(:branches).and_return([]) end - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /:project_path/tags" do - subject { project_tags_path(project) } + subject { namespace_project_tags_path(project.namespace, project) } before do # Speed increase - Project.any_instance.stub(:tags).and_return([]) + allow_any_instance_of(Project).to receive(:tags).and_return([]) end - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /:project_path/hooks" do - subject { project_hooks_path(project) } - - it { should be_allowed_for master } - it { should be_denied_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } + subject { namespace_project_hooks_path(project.namespace, project) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } end end diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb new file mode 100644 index 00000000000..2099fc40cca --- /dev/null +++ b/spec/features/task_lists_spec.rb @@ -0,0 +1,151 @@ +require 'spec_helper' + +feature 'Task Lists' do + include Warden::Test::Helpers + + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + + let(:markdown) do + <<-MARKDOWN.strip_heredoc + This is a task list: + + - [ ] Incomplete entry 1 + - [x] Complete entry 1 + - [ ] Incomplete entry 2 + - [x] Complete entry 2 + - [ ] Incomplete entry 3 + - [ ] Incomplete entry 4 + MARKDOWN + end + + before do + Warden.test_mode! + + project.team << [user, :master] + project.team << [user2, :guest] + + login_as(user) + end + + def visit_issue(project, issue) + visit namespace_project_issue_path(project.namespace, project, issue) + end + + describe 'for Issues' do + let!(:issue) { create(:issue, description: markdown, author: user, project: project) } + + it 'renders' do + visit_issue(project, issue) + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 6) + expect(page).to have_selector('ul input[checked]', count: 2) + end + + it 'contains the required selectors' do + visit_issue(project, issue) + + container = '.issue-details .description.js-task-list-container' + + expect(page).to have_selector(container) + expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector("#{container} .js-task-list-field") + expect(page).to have_selector('form.js-issue-update') + expect(page).to have_selector('a.btn-close') + end + + it 'is only editable by author' do + visit_issue(project, issue) + expect(page).to have_selector('.js-task-list-container') + + logout(:user) + + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end + + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("6 tasks (2 completed, 4 remaining)") + end + end + + describe 'for Notes' do + let!(:issue) { create(:issue, author: user, project: project) } + let!(:note) { create(:note, note: markdown, noteable: issue, author: user) } + + it 'renders for note body' do + visit_issue(project, issue) + + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 6) + expect(page).to have_selector('.note ul input[checked]', count: 2) + end + + it 'contains the required selectors' do + visit_issue(project, issue) + + expect(page).to have_selector('.note .js-task-list-container') + expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox') + expect(page).to have_selector('.note .js-task-list-container .js-task-list-field') + end + + it 'is only editable by author' do + visit_issue(project, issue) + expect(page).to have_selector('.js-task-list-container') + + logout(:user) + + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end + end + + describe 'for Merge Requests' do + def visit_merge_request(project, merge) + visit namespace_project_merge_request_path(project.namespace, project, merge) + end + + let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) } + + it 'renders for description' do + visit_merge_request(project, merge) + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 6) + expect(page).to have_selector('ul input[checked]', count: 2) + end + + it 'contains the required selectors' do + visit_merge_request(project, merge) + + container = '.merge-request-details .description.js-task-list-container' + + expect(page).to have_selector(container) + expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector("#{container} .js-task-list-field") + expect(page).to have_selector('form.js-merge-request-update') + expect(page).to have_selector('a.btn-close') + end + + it 'is only editable by author' do + visit_merge_request(project, merge) + expect(page).to have_selector('.js-task-list-container') + + logout(:user) + + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end + + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("6 tasks (2 completed, 4 remaining)") + end + end +end diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index a1206989d39..a4c3dfe9205 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -1,19 +1,51 @@ require 'spec_helper' -describe 'Users', feature: true do - describe "GET /users/sign_up" do - before do - Gitlab.config.gitlab.stub(:signup_enabled).and_return(true) - end - - it "should create a new user account" do - visit new_user_registration_path - fill_in "user_name", with: "Name Surname" - fill_in "user_username", with: "Great" - fill_in "user_email", with: "name@mail.com" - fill_in "user_password_sign_up", with: "password1234" - fill_in "user_password_confirmation", with: "password1234" - expect { click_button "Sign up" }.to change {User.count}.by(1) - end +feature 'Users' do + scenario 'GET /users/sign_in creates a new user account' do + visit new_user_session_path + fill_in 'user_name', with: 'Name Surname' + fill_in 'user_username', with: 'Great' + fill_in 'user_email', with: 'name@mail.com' + fill_in 'user_password_sign_up', with: 'password1234' + expect { click_button 'Sign up' }.to change { User.count }.by(1) end + + scenario 'Successful user signin invalidates password reset token' do + user = create(:user) + expect(user.reset_password_token).to be_nil + + visit new_user_password_path + fill_in 'user_email', with: user.email + click_button 'Reset password' + + user.reload + expect(user.reset_password_token).not_to be_nil + + login_with(user) + expect(current_path).to eq root_path + + user.reload + expect(user.reset_password_token).to be_nil + end + + let!(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } + scenario 'Should show one error if email is already taken' do + visit new_user_session_path + fill_in 'user_name', with: 'Another user name' + fill_in 'user_username', with: 'anotheruser' + fill_in 'user_email', with: user.email + fill_in 'user_password_sign_up', with: '12341234' + expect { click_button 'Sign up' }.to change { User.count }.by(0) + expect(page).to have_text('Email has already been taken') + expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}' + end + + def errors_on_page(page) + page.find('#error_explanation').find('ul').all('li').map{ |item| item.text }.join("\n") + end + + def number_of_errors_on_page(page) + page.find('#error_explanation').find('ul').all('li').count + end + end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 7489e56f423..db20b23f87d 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -5,9 +5,10 @@ describe IssuesFinder do let(:user2) { create :user } let(:project1) { create(:project) } let(:project2) { create(:project) } - let(:issue1) { create(:issue, assignee: user, project: project1) } - let(:issue2) { create(:issue, assignee: user, project: project2) } - let(:issue3) { create(:issue, assignee: user2, project: project2) } + let(:milestone) { create(:milestone, project: project1) } + let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) } + let(:issue2) { create(:issue, author: user, assignee: user, project: project2) } + let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) } before do project1.team << [user, :master] @@ -22,37 +23,59 @@ describe IssuesFinder do issue3 end - it 'should filter by all' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new.execute(user, params) - issues.size.should == 3 - end + 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) + end - it 'should filter by assignee' do - params = { scope: "assigned-to-me", state: 'opened' } - issues = IssuesFinder.new.execute(user, params) - issues.size.should == 2 - 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) + end - it 'should filter by project' do - params = { scope: "assigned-to-me", state: 'opened', project_id: project1.id } - issues = IssuesFinder.new.execute(user, params) - issues.size.should == 1 - 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]) + end - it 'should be empty for unauthorized user' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new.execute(nil, params) - issues.size.should be_zero + 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]) + 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 + 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) + end end - it 'should not include unauthorized issues' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new.execute(user2, params) - issues.size.should == 2 - issues.should_not include(issue1) - issues.should include(issue2) - issues.should include(issue3) + 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) + 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) + end end end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 94b4d4c4ff4..bc385fd0d69 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -20,14 +20,14 @@ describe MergeRequestsFinder do describe "#execute" do it 'should filter by scope' do params = { scope: 'authored', state: 'opened' } - merge_requests = MergeRequestsFinder.new.execute(user, params) - merge_requests.size.should == 2 + merge_requests = MergeRequestsFinder.new(user, params).execute + expect(merge_requests.size).to eq(2) end it 'should filter by project' do params = { project_id: project1.id, scope: 'authored', state: 'opened' } - merge_requests = MergeRequestsFinder.new.execute(user, params) - merge_requests.size.should == 1 + merge_requests = MergeRequestsFinder.new(user, params).execute + expect(merge_requests.size).to eq(1) end end end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 4f8a5f909df..c83824b900d 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -21,7 +21,7 @@ describe NotesFinder do it 'should find all notes' do notes = NotesFinder.new.execute(project, user, params) - notes.size.should eq(2) + expect(notes.size).to eq(2) end it 'should raise an exception for an invalid target_type' do @@ -32,7 +32,7 @@ describe NotesFinder do it 'filters out old notes' do note2.update_attribute(:updated_at, 2.hours.ago) notes = NotesFinder.new.execute(project, user, params) - notes.should eq([note1]) + expect(notes).to eq([note1]) end end end diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index 6e3ae4d615b..2ab71b05968 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -12,19 +12,19 @@ describe ProjectsFinder do context 'non authenticated' do subject { ProjectsFinder.new.execute(nil, group: group) } - it { should include(project1) } - it { should_not include(project2) } - it { should_not include(project3) } - it { should_not include(project4) } + it { is_expected.to include(project1) } + it { is_expected.not_to include(project2) } + it { is_expected.not_to include(project3) } + it { is_expected.not_to include(project4) } end context 'authenticated' do subject { ProjectsFinder.new.execute(user, group: group) } - it { should include(project1) } - it { should include(project2) } - it { should_not include(project3) } - it { should_not include(project4) } + it { is_expected.to include(project1) } + it { is_expected.to include(project2) } + it { is_expected.not_to include(project3) } + it { is_expected.not_to include(project4) } end context 'authenticated, project member' do @@ -32,10 +32,10 @@ describe ProjectsFinder do subject { ProjectsFinder.new.execute(user, group: group) } - it { should include(project1) } - it { should include(project2) } - it { should include(project3) } - it { should_not include(project4) } + it { is_expected.to include(project1) } + it { is_expected.to include(project2) } + it { is_expected.to include(project3) } + it { is_expected.not_to include(project4) } end context 'authenticated, group member' do @@ -43,9 +43,9 @@ describe ProjectsFinder do subject { ProjectsFinder.new.execute(user, group: group) } - it { should include(project1) } - it { should include(project2) } - it { should include(project3) } - it { should include(project4) } + it { is_expected.to include(project1) } + it { is_expected.to include(project2) } + it { is_expected.to include(project3) } + it { is_expected.to include(project4) } end end diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index c645cbc964c..1b4ffc2d717 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -18,14 +18,14 @@ describe SnippetsFinder do it "returns all private and internal snippets" do snippets = SnippetsFinder.new.execute(user, filter: :all) - snippets.should include(@snippet2, @snippet3) - snippets.should_not include(@snippet1) + expect(snippets).to include(@snippet2, @snippet3) + expect(snippets).not_to include(@snippet1) end it "returns all public snippets" do snippets = SnippetsFinder.new.execute(nil, filter: :all) - snippets.should include(@snippet3) - snippets.should_not include(@snippet1, @snippet2) + expect(snippets).to include(@snippet3) + expect(snippets).not_to include(@snippet1, @snippet2) end end @@ -38,37 +38,37 @@ describe SnippetsFinder do it "returns all public and internal snippets" do snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user) - snippets.should include(@snippet2, @snippet3) - snippets.should_not include(@snippet1) + expect(snippets).to include(@snippet2, @snippet3) + expect(snippets).not_to include(@snippet1) end it "returns internal snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal") - snippets.should include(@snippet2) - snippets.should_not include(@snippet1, @snippet3) + expect(snippets).to include(@snippet2) + expect(snippets).not_to include(@snippet1, @snippet3) end it "returns private snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private") - snippets.should include(@snippet1) - snippets.should_not include(@snippet2, @snippet3) + expect(snippets).to include(@snippet1) + expect(snippets).not_to include(@snippet2, @snippet3) end it "returns public snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public") - snippets.should include(@snippet3) - snippets.should_not include(@snippet1, @snippet2) + expect(snippets).to include(@snippet3) + expect(snippets).not_to include(@snippet1, @snippet2) end it "returns all snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user) - snippets.should include(@snippet1, @snippet2, @snippet3) + expect(snippets).to include(@snippet1, @snippet2, @snippet3) end it "returns only public snippets if unauthenticated user" do snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user) - snippets.should include(@snippet3) - snippets.should_not include(@snippet2, @snippet1) + expect(snippets).to include(@snippet3) + expect(snippets).not_to include(@snippet2, @snippet1) end end @@ -82,20 +82,20 @@ describe SnippetsFinder do it "returns public snippets for unauthorized user" do snippets = SnippetsFinder.new.execute(nil, filter: :by_project, project: project1) - snippets.should include(@snippet3) - snippets.should_not include(@snippet1, @snippet2) + expect(snippets).to include(@snippet3) + expect(snippets).not_to include(@snippet1, @snippet2) end it "returns public and internal snippets for none project members" do snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) - snippets.should include(@snippet2, @snippet3) - snippets.should_not include(@snippet1) + expect(snippets).to include(@snippet2, @snippet3) + expect(snippets).not_to include(@snippet1) end it "returns all snippets for project members" do project1.team << [user, :developer] snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) - snippets.should include(@snippet1, @snippet2, @snippet3) + expect(snippets).to include(@snippet1, @snippet2, @snippet3) end end end diff --git a/spec/fixtures/GoogleCodeProjectHosting.json b/spec/fixtures/GoogleCodeProjectHosting.json new file mode 100644 index 00000000000..d05e77271ae --- /dev/null +++ b/spec/fixtures/GoogleCodeProjectHosting.json @@ -0,0 +1,407 @@ +{ + "kind" : "projecthosting#user", + "id" : "@WRRVSlFXARlCVgB6", + "projects" : [ { + "kind" : "projecthosting#project", + "name" : "pmn", + "externalId" : "pmn", + "htmlLink" : "/p/pmn/", + "summary" : "Shows an icon in the system tray when you have new emails", + "description" : "IMAP client that shows an icon in the system tray when you have new emails.", + "labels" : [ "Mail" ], + "versionControlSystem" : "svn", + "repositoryUrls" : [ "https://pmn.googlecode.com/svn/" ], + "issuesConfig" : { + "kind" : "projecthosting#projectIssueConfig", + "statuses" : [ { + "status" : "New", + "meansOpen" : true, + "description" : "Issue has not had initial review yet" + }, { + "status" : "Accepted", + "meansOpen" : true, + "description" : "Problem reproduced / Need acknowledged" + }, { + "status" : "Started", + "meansOpen" : true, + "description" : "Work on this issue has begun" + }, { + "status" : "Fixed", + "meansOpen" : false, + "description" : "Developer made source code changes, QA should verify" + }, { + "status" : "Verified", + "meansOpen" : false, + "description" : "QA has verified that the fix worked" + }, { + "status" : "Invalid", + "meansOpen" : false, + "description" : "This was not a valid issue report" + }, { + "status" : "Duplicate", + "meansOpen" : false, + "description" : "This report duplicates an existing issue" + }, { + "status" : "WontFix", + "meansOpen" : false, + "description" : "We decided to not take action on this issue" + }, { + "status" : "Done", + "meansOpen" : false, + "description" : "The requested non-coding task was completed" + } ], + "labels" : [ { + "label" : "Type-Defect", + "description" : "Report of a software defect" + }, { + "label" : "Type-Enhancement", + "description" : "Request for enhancement" + }, { + "label" : "Type-Task", + "description" : "Work item that doesn't change the code or docs" + }, { + "label" : "Type-Review", + "description" : "Request for a source code review" + }, { + "label" : "Type-Other", + "description" : "Some other kind of issue" + }, { + "label" : "Priority-Critical", + "description" : "Must resolve in the specified milestone" + }, { + "label" : "Priority-High", + "description" : "Strongly want to resolve in the specified milestone" + }, { + "label" : "Priority-Medium", + "description" : "Normal priority" + }, { + "label" : "Priority-Low", + "description" : "Might slip to later milestone" + }, { + "label" : "OpSys-All", + "description" : "Affects all operating systems" + }, { + "label" : "OpSys-Windows", + "description" : "Affects Windows users" + }, { + "label" : "OpSys-Linux", + "description" : "Affects Linux users" + }, { + "label" : "OpSys-OSX", + "description" : "Affects Mac OS X users" + }, { + "label" : "Milestone-Release1.0", + "description" : "All essential functionality working" + }, { + "label" : "Component-UI", + "description" : "Issue relates to program UI" + }, { + "label" : "Component-Logic", + "description" : "Issue relates to application logic" + }, { + "label" : "Component-Persistence", + "description" : "Issue relates to data storage components" + }, { + "label" : "Component-Scripts", + "description" : "Utility and installation scripts" + }, { + "label" : "Component-Docs", + "description" : "Issue relates to end-user documentation" + }, { + "label" : "Security", + "description" : "Security risk to users" + }, { + "label" : "Performance", + "description" : "Performance issue" + }, { + "label" : "Usability", + "description" : "Affects program usability" + }, { + "label" : "Maintainability", + "description" : "Hinders future changes" + } ], + "prompts" : [ { + "name" : "Defect report from user", + "title" : "Enter one-line summary", + "description" : "What steps will reproduce the problem?\n1. \n2. \n3. \n\nWhat is the expected output? What do you see instead?\n\n\nWhat version of the product are you using? On what operating system?\n\n\nPlease provide any additional information below.\n", + "titleMustBeEdited" : true, + "status" : "New", + "labels" : [ "Type-Defect", "Priority-Medium" ] + }, { + "name" : "Defect report from developer", + "title" : "Enter one-line summary", + "description" : "What steps will reproduce the problem?\n1. \n2. \n3. \n\nWhat is the expected output? What do you see instead?\n\n\nPlease use labels and text to provide additional information.\n", + "titleMustBeEdited" : true, + "status" : "Accepted", + "labels" : [ "Type-Defect", "Priority-Medium" ], + "membersOnly" : true + }, { + "name" : "Review request", + "title" : "Code review request", + "description" : "Branch name:\n\nPurpose of code changes on this branch:\n\n\nWhen reviewing my code changes, please focus on:\n\n\nAfter the review, I'll merge this branch into:\n/trunk\n", + "status" : "New", + "labels" : [ "Type-Review", "Priority-Medium" ], + "membersOnly" : true, + "defaultToMember" : false + } ], + "defaultPromptForMembers" : 1, + "defaultPromptForNonMembers" : 0 + }, + "role" : "owner", + "members" : [ { + "kind" : "projecthosting#issuePerson", + "name" : "mrovi9000", + "htmlLink" : "https://code.google.com/u/106736353629303906862/" + } ], + "issues" : { + "kind" : "projecthosting#issueList", + "totalResults" : 0, + "items" : [ ] + } + }, { + "kind" : "projecthosting#project", + "name" : "tint2", + "externalId" : "tint2", + "htmlLink" : "/p/tint2/", + "summary" : "tint2 is a lightweight panel/taskbar.", + "description" : "tint2 is a simple _*panel/taskbar*_ unintrusive and light (memory / cpu / aestetic). <br>We follow freedesktop specifications.\r\n \r\n=== 0.11 features ===\r\n * panel with taskbar, systray, clock and battery status\r\n * easy to customize : color/transparency on font, icon, border and background\r\n * pager like capability : send task from one workspace to another, switch workspace\r\n * multi-monitor capability : one panel per monitor, show task from current monitor\r\n * customize mouse event\r\n * window manager's menu\r\n * tooltip\r\n * autohide\r\n * clock timezones\r\n * real & fake transparency with autodetection of composite manager\r\n * panel's theme switcher 'tint2conf' \r\n\r\n=== Other project ===\r\n * Lightweight volume control http://softwarebakery.com/maato/volumeicon.html\r\n * Lightweight calendar http://code.google.com/p/gsimplecal/\r\n * Graphical config tool http://code.google.com/p/tintwizard/\r\n * Command line theme switcher http://github.com/dbbolton/scripts/blob/master/tint2theme\r\n\r\n\r\n=== Snapshot SVN ===\r\n\r\nhttp://img252.imageshack.us/img252/1433/wallpaper2td.jpg\r\n\r\n\r\n", + "labels" : [ "taskbar", "panel", "lightweight", "desktop", "openbox", "pager", "tint2" ], + "versionControlSystem" : "git", + "repositoryUrls" : [ "https://tint2.googlecode.com/git/" ], + "issuesConfig" : { + "kind" : "projecthosting#projectIssueConfig", + "defaultColumns" : [ "ID", "Status", "Type", "Milestone", "Priority", "Component", "Owner", "Summary", "Modified", "Stars" ], + "defaultSorting" : [ "-ID" ], + "statuses" : [ { + "status" : "New", + "meansOpen" : true, + "description" : "Issue has not had initial review yet" + }, { + "status" : "NeedInfo", + "meansOpen" : true, + "description" : "More information is needed before deciding what action should be taken" + }, { + "status" : "Accepted", + "meansOpen" : true, + "description" : "A Defect that a developer has reproduced or an Enhancement that a developer has committed to addressing" + }, { + "status" : "Wishlist", + "meansOpen" : true, + "description" : "An Enhancement which is valid, but no developers have committed to addressing" + }, { + "status" : "Started", + "meansOpen" : true, + "description" : "Work on this issue has begun" + }, { + "status" : "Fixed", + "meansOpen" : false, + "description" : "Work has completed" + }, { + "status" : "Invalid", + "meansOpen" : false, + "description" : "This was not a valid issue report" + }, { + "status" : "Duplicate", + "meansOpen" : false, + "description" : "This report duplicates an existing issue" + }, { + "status" : "WontFix", + "meansOpen" : false, + "description" : "We decided to not take action on this issue" + }, { + "status" : "Incomplete", + "meansOpen" : false, + "description" : "Not enough information and no activity for a long period of time" + } ], + "labels" : [ { + "label" : "Type-Defect", + "description" : "Report of a software defect" + }, { + "label" : "Type-Enhancement", + "description" : "Request for enhancement" + }, { + "label" : "Type-Task", + "description" : "Work item that does not change the code" + }, { + "label" : "Type-Review", + "description" : "Request for a source code review" + }, { + "label" : "Type-Other", + "description" : "Some other kind of issue" + }, { + "label" : "Milestone-0.12", + "description" : "Fix should be included in release 0.12" + }, { + "label" : "Priority-Critical", + "description" : "Must resolve in the specified milestone" + }, { + "label" : "Priority-High", + "description" : "Strongly want to resolve in the specified milestone" + }, { + "label" : "Priority-Medium", + "description" : "Normal priority" + }, { + "label" : "Priority-Low", + "description" : "Might slip to later milestone" + }, { + "label" : "OpSys-All", + "description" : "Affects all operating systems" + }, { + "label" : "OpSys-Windows", + "description" : "Affects Windows users" + }, { + "label" : "OpSys-Linux", + "description" : "Affects Linux users" + }, { + "label" : "OpSys-OSX", + "description" : "Affects Mac OS X users" + }, { + "label" : "Security", + "description" : "Security risk to users" + }, { + "label" : "Performance", + "description" : "Performance issue" + }, { + "label" : "Usability", + "description" : "Affects program usability" + }, { + "label" : "Maintainability", + "description" : "Hinders future changes" + }, { + "label" : "Component-Panel", + "description" : "Issue relates to the panel (e.g. positioning, hiding, transparency)" + }, { + "label" : "Component-Taskbar", + "description" : "Issue relates to the taskbar (e.g. tasks, multiple desktops)" + }, { + "label" : "Component-Battery", + "description" : "Issue relates to the battery" + }, { + "label" : "Component-Systray", + "description" : "Issue relates to the system tray" + }, { + "label" : "Component-Clock", + "description" : "Issue relates to the clock" + }, { + "label" : "Component-Launcher", + "description" : "Issue relates to the launcher" + }, { + "label" : "Component-Tint2conf", + "description" : "Issue relates to the configuration GUI (tint2conf)" + }, { + "label" : "Component-Docs", + "description" : "Issue relates to end-user documentation" + }, { + "label" : "Component-New", + "description" : "Issue describes a new component proposal" + } ], + "prompts" : [ { + "name" : "Defect report from user", + "title" : "Enter one-line summary", + "description" : "What steps will reproduce the problem?\n1.\n2.\n3.\n\nWhat is the expected output? What do you see instead?\n\n\nWhat version of the product are you using? On what operating system?\n\n\nWhich window manager (e.g. openbox, xfwm, metacity, mutter, kwin) or\nwhich desktop environment (e.g. Gnome 2, Gnome 3, LXDE, XFCE, KDE)\nare you using?\n\n\nPlease provide any additional information below. It might be helpful\nto attach your tint2rc file (usually located at ~/.config/tint2/tint2rc).", + "titleMustBeEdited" : true, + "status" : "New", + "labels" : [ "Priority-Medium" ], + "defaultToMember" : true + }, { + "name" : "Defect report from developer", + "title" : "Enter one-line summary", + "description" : "What steps will reproduce the problem?\n1.\n2.\n3.\n\nWhat is the expected output? What do you see instead?\n\n\nPlease use labels and text to provide additional information.", + "titleMustBeEdited" : true, + "status" : "Accepted", + "labels" : [ "Type-Defect", "Priority-Medium" ], + "membersOnly" : true, + "defaultToMember" : true + }, { + "name" : "Review request", + "title" : "Code review request", + "description" : "Purpose of code changes on this branch:\n\n\nWhen reviewing my code changes, please focus on:\n\n\nAfter the review, I'll merge this branch into:\n/trunk", + "status" : "New", + "labels" : [ "Type-Review", "Priority-Medium" ], + "membersOnly" : true, + "defaultToMember" : true + } ], + "defaultPromptForMembers" : 1, + "defaultPromptForNonMembers" : 0, + "usersCanSetLabels" : false + }, + "role" : "owner", + "issues" : { + "kind" : "projecthosting#issueList", + "totalResults" : 473, + "items" : [ { + "kind" : "projecthosting#issue", + "id" : 169, + "title" : "Scrolling through tasks", + "summary" : "Scrolling through tasks", + "stars" : 1, + "starred" : false, + "status" : "Fixed", + "state" : "closed", + "labels" : [ "Type-Enhancement", "Priority-Medium" ], + "author" : { + "kind" : "projecthosting#issuePerson", + "name" : "schattenpr...", + "htmlLink" : "https://code.google.com/u/106498139506637530000/" + }, + "owner" : { + "kind" : "projecthosting#issuePerson", + "name" : "thilo...", + "htmlLink" : "https://code.google.com/u/104224918623172014000/" + }, + "updated" : "2009-11-18T05:14:58.000Z", + "published" : "2009-11-18T00:20:19.000Z", + "closed" : "2009-11-18T05:14:58.000Z", + "projectId" : "tint2", + "canComment" : true, + "canEdit" : true, + "comments" : { + "kind" : "projecthosting#issueCommentList", + "totalResults" : 2, + "items" : [ { + "id" : 0, + "kind" : "projecthosting#issueComment", + "author" : { + "kind" : "projecthosting#issuePerson", + "name" : "schattenpr...", + "htmlLink" : "https://code.google.com/u/10649813950663753000/" + }, + "content" : "I like to scroll through the tasks with my scrollwheel (like in fluxbox). \r\n\r\nPatch is attached that adds two new mouse-actions (next_task+prev_task) \r\nthat can be used for exactly that purpose. \r\n\r\nall the best!", + "published" : "2009-11-18T00:20:19.000Z", + "updates" : { + "kind" : "projecthosting#issueCommentUpdate" + }, + "canDelete" : true, + "attachments" : [ { + "attachmentId" : "8901002890399325565", + "fileName" : "tint2_task_scrolling.diff", + "fileSize" : 3059, + "mimetype" : "text/x-c++; charset=us-ascii" + }, { + "attachmentId" : "000", + "fileName" : "screenshot.png", + "fileSize" : 0, + "mimetype" : "image/png" + } ] + }, { + "id" : 1, + "kind" : "projecthosting#issueComment", + "author" : { + "kind" : "projecthosting#issuePerson", + "name" : "thilo...", + "htmlLink" : "https://code.google.com/u/104224918623172014000/" + }, + "content" : "applied, thanks.\r\n", + "published" : "2009-11-18T05:14:58.000Z", + "updates" : { + "kind" : "projecthosting#issueCommentUpdate", + "status" : "Fixed", + "labels" : [ "-Type-Defect", "Type-Enhancement" ] + }, + "canDelete" : true + } ] + } + } ] + } + } ] +} diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb new file mode 100644 index 00000000000..02ab46c905a --- /dev/null +++ b/spec/fixtures/markdown.md.erb @@ -0,0 +1,201 @@ +# GitLab Markdown + +This document is intended to be a comprehensive example of custom GitLab +Markdown usage. It will be parsed and then tested for accuracy. Let's get +started. + +## Markdown + +GitLab uses [Redcarpet](http://git.io/ld_NVQ) to parse all Markdown into +HTML. + +It has some special features. Let's try 'em out! + +### No Intra Emphasis + +This string should have no emphasis: foo_bar_baz + +### Tables + +| Header | Row | Example | +| :------: | ---: | :------ | +| Foo | Bar | **Baz** | + +### Fenced Code Blocks + +```c +#include<stdio.h> + +main() +{ + printf("Hello World"); + +} +``` + +```python +print "Hello, World!" +``` + +### Strikethrough + +This text says this, ~~and this text doesn't~~. + +### Superscript + +This is my 1^(st) time using superscript in Markdown. Now this is my +2^(nd). + +### Next step + +After the Markdown has been turned into HTML, it gets passed through... + +## HTML::Pipeline + +### SanitizationFilter + +GitLab uses <a href="http://git.io/vfW8a">HTML::Pipeline::SanitizationFilter</a> +to sanitize the generated HTML, stripping dangerous or unwanted tags. + +Its default whitelist is pretty permissive. Check it: + +<b>b tag</b> and <em>em tag</em>. + +<code>code tag</code> + +Press <kbd>s</kbd> to search. + +<strike>Emoji</strike> Plain old images! <img src="http://www.emoji-cheat-sheet.com/graphics/emojis/smile.png" width="20" height="20" /> + +Here comes a line break: + +<br /> + +And a horizontal rule: + +<hr /> + +As permissive as it is, we've allowed even more stuff: + +<span>span tag</span> + +<a href="#" rel="bookmark">This is a link with a defined rel attribute, which should be removed</a> + +<a href="javascript:alert('Hi')">This is a link trying to be sneaky. It gets its link removed entirely.</a> + +### Escaping + +The problem with SanitizationFilter is that it can be too aggressive. + +| Input | Expected | Actual | +| ----------- | ---------------- | --------- | +| `1 < 3 & 5` | 1 < 3 & 5 | 1 < 3 & 5 | +| `<foo>` | <foo> | <foo> | + +### Edge Cases + +Markdown should be usable inside a link. Let's try! + +- [_text_](#link-emphasis) +- [**text**](#link-strong) +- [`text`](#link-code) + +### EmojiFilter + +Because life would be :zzz: without Emoji, right? :rocket: + +Get ready for the Emoji :bomb:: :+1::-1::ok_hand::wave::v::raised_hand::muscle: + +### TableOfContentsFilter + +All headers in this document should be linkable. Try it. + +### AutolinkFilter + +These are all plain text that should get turned into links: + +- http://about.gitlab.com/ +- https://google.com/ +- ftp://ftp.us.debian.org/debian/ +- smb://foo/bar/baz +- irc://irc.freenode.net/git +- http://localhost:3000 + +But it shouldn't autolink text inside certain tags: + +- <code>http://about.gitlab.com/</code> +- <a>http://about.gitlab.com/</a> +- <kbd>http://about.gitlab.com/</kbd> + +### ExternalLinkFilter + +External links get a `rel="nofollow"` attribute: + +- [Google](https://google.com/) +- [GitLab Root](<%= Gitlab.config.gitlab.url %>) + +### Reference Filters (e.g., <%= issue.to_reference %>) + +References should be parseable even inside _<%= merge_request.to_reference %>_ emphasis. + +#### UserReferenceFilter + +- All: @all +- User: <%= user.to_reference %> +- Group: <%= group.to_reference %> +- Ignores invalid: <%= User.reference_prefix %>fake_user +- Ignored in code: `<%= user.to_reference %>` +- Ignored in links: [Link to <%= user.to_reference %>](#user-link) + +#### IssueReferenceFilter + +- Issue: <%= issue.to_reference %> +- Issue in another project: <%= xissue.to_reference(project) %> +- Ignored in code: `<%= issue.to_reference %>` +- Ignored in links: [Link to <%= issue.to_reference %>](#issue-link) + +#### MergeRequestReferenceFilter + +- Merge request: <%= merge_request.to_reference %> +- Merge request in another project: <%= xmerge_request.to_reference(project) %> +- Ignored in code: `<%= merge_request.to_reference %>` +- Ignored in links: [Link to <%= merge_request.to_reference %>](#merge-request-link) + +#### SnippetReferenceFilter + +- Snippet: <%= snippet.to_reference %> +- Snippet in another project: <%= xsnippet.to_reference(project) %> +- Ignored in code: `<%= snippet.to_reference %>` +- Ignored in links: [Link to <%= snippet.to_reference %>](#snippet-link) + +#### CommitRangeReferenceFilter + +- Range: <%= commit_range.to_reference %> +- Range in another project: <%= xcommit_range.to_reference(project) %> +- Ignored in code: `<%= commit_range.to_reference %>` +- Ignored in links: [Link to <%= commit_range.to_reference %>](#commit-range-link) + +#### CommitReferenceFilter + +- Commit: <%= commit.to_reference %> +- Commit in another project: <%= xcommit.to_reference(project) %> +- Ignored in code: `<%= commit.to_reference %>` +- Ignored in links: [Link to <%= commit.to_reference %>](#commit-link) + +#### LabelReferenceFilter + +- Label by ID: <%= simple_label.to_reference %> +- Label by name: <%= Label.reference_prefix %><%= simple_label.name %> +- Label by name in quotes: <%= label.to_reference(:name) %> +- Ignored in code: `<%= simple_label.to_reference %>` +- Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link) + +### Task Lists + +- [ ] Incomplete task 1 +- [x] Complete task 1 +- [ ] Incomplete task 2 + - [ ] Incomplete sub-task 1 + - [ ] Incomplete sub-task 2 + - [x] Complete sub-task 1 +- [X] Complete task 2 diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 2db67cfdf95..582c401c55a 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -3,20 +3,20 @@ require 'spec_helper' describe ApplicationHelper do describe 'current_controller?' do before do - controller.stub(:controller_name).and_return('foo') + allow(controller).to receive(:controller_name).and_return('foo') end - it "returns true when controller matches argument" do - current_controller?(:foo).should be_true + it 'returns true when controller matches argument' do + expect(current_controller?(:foo)).to be_truthy end - it "returns false when controller does not match argument" do - current_controller?(:bar).should_not be_true + it 'returns false when controller does not match argument' do + expect(current_controller?(:bar)).not_to be_truthy end - it "should take any number of arguments" do - current_controller?(:baz, :bar).should_not be_true - current_controller?(:baz, :bar, :foo).should be_true + it 'should take any number of arguments' do + expect(current_controller?(:baz, :bar)).not_to be_truthy + expect(current_controller?(:baz, :bar, :foo)).to be_truthy end end @@ -25,99 +25,124 @@ describe ApplicationHelper do allow(self).to receive(:action_name).and_return('foo') end - it "returns true when action matches argument" do - current_action?(:foo).should be_true + it 'returns true when action matches argument' do + expect(current_action?(:foo)).to be_truthy end - it "returns false when action does not match argument" do - current_action?(:bar).should_not be_true + it 'returns false when action does not match argument' do + expect(current_action?(:bar)).not_to be_truthy end - it "should take any number of arguments" do - current_action?(:baz, :bar).should_not be_true - current_action?(:baz, :bar, :foo).should be_true + it 'should take any number of arguments' do + expect(current_action?(:baz, :bar)).not_to be_truthy + expect(current_action?(:baz, :bar, :foo)).to be_truthy end end - describe "group_icon" do + describe 'project_icon' do avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png') - it "should return an url for the avatar" do - group = create(:group) - group.avatar = File.open(avatar_file_path) - group.save! - group_icon(group.path).to_s.should match("/uploads/group/avatar/#{ group.id }/gitlab_logo.png") + it 'should return an url for the avatar' do + project = create(:project) + project.avatar = File.open(avatar_file_path) + project.save! + avatar_url = "http://localhost/uploads/project/avatar/#{ project.id }/gitlab_logo.png" + expect(project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to eq( + "<img alt=\"Gitlab logo\" src=\"#{avatar_url}\" />" + ) end - it "should give default avatar_icon when no avatar is present" do - group = create(:group) - group.save! - group_icon(group.path).should match("group_avatar.png") + it 'should give uploaded icon when present' do + project = create(:project) + project.save! + + allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true) + + avatar_url = 'http://localhost' + namespace_project_avatar_path(project.namespace, project) + expect(project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to match( + image_tag(avatar_url)) end end - describe "avatar_icon" do + describe 'avatar_icon' do avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png') - it "should return an url for the avatar" do + it 'should return an url for the avatar' do + user = create(:user) + user.avatar = File.open(avatar_file_path) + user.save! + expect(avatar_icon(user.email).to_s). + to match("/uploads/user/avatar/#{ user.id }/gitlab_logo.png") + end + + it 'should return an url for the avatar with relative url' do + allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab') + allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) + user = create(:user) user.avatar = File.open(avatar_file_path) user.save! - avatar_icon(user.email).to_s.should match("/uploads/user/avatar/#{ user.id }/gitlab_logo.png") + expect(avatar_icon(user.email).to_s). + to match("/gitlab/uploads/user/avatar/#{ user.id }/gitlab_logo.png") end - it "should call gravatar_icon when no avatar is present" do + it 'should call gravatar_icon when no avatar is present' do user = create(:user, email: 'test@example.com') user.save! - avatar_icon(user.email).to_s.should == "http://www.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=40&d=identicon" + expect(avatar_icon(user.email).to_s).to eq('http://www.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=40&d=identicon') end end - describe "gravatar_icon" do + describe 'gravatar_icon' do let(:user_email) { 'user@email.com' } - it "should return a generic avatar path when Gravatar is disabled" do - Gitlab.config.gravatar.stub(:enabled).and_return(false) - gravatar_icon(user_email).should match('no_avatar.png') + it 'should return a generic avatar path when Gravatar is disabled' do + allow_any_instance_of(ApplicationSetting).to receive(:gravatar_enabled?).and_return(false) + expect(gravatar_icon(user_email)).to match('no_avatar.png') end - it "should return a generic avatar path when email is blank" do - gravatar_icon('').should match('no_avatar.png') + it 'should return a generic avatar path when email is blank' do + expect(gravatar_icon('')).to match('no_avatar.png') end - it "should return default gravatar url" do - Gitlab.config.gitlab.stub(https: false) - gravatar_icon(user_email).should match('http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118') + it 'should return default gravatar url' do + allow(Gitlab.config.gitlab).to receive(:https).and_return(false) + url = 'http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118' + expect(gravatar_icon(user_email)).to match(url) end - it "should use SSL when appropriate" do - Gitlab.config.gitlab.stub(https: true) - gravatar_icon(user_email).should match('https://secure.gravatar.com') + it 'should use SSL when appropriate' do + allow(Gitlab.config.gitlab).to receive(:https).and_return(true) + expect(gravatar_icon(user_email)).to match('https://secure.gravatar.com') end - it "should return custom gravatar path when gravatar_url is set" do + it 'should return custom gravatar path when gravatar_url is set' do allow(self).to receive(:request).and_return(double(:ssl? => false)) - Gitlab.config.gravatar.stub(:plain_url).and_return('http://example.local/?s=%{size}&hash=%{hash}') - gravatar_icon(user_email, 20).should == 'http://example.local/?s=20&hash=b58c6f14d292556214bd64909bcdb118' + allow(Gitlab.config.gravatar). + to receive(:plain_url). + and_return('http://example.local/?s=%{size}&hash=%{hash}') + url = 'http://example.local/?s=20&hash=b58c6f14d292556214bd64909bcdb118' + expect(gravatar_icon(user_email, 20)).to eq(url) end - it "should accept a custom size" do + it 'should accept a custom size' do allow(self).to receive(:request).and_return(double(:ssl? => false)) - gravatar_icon(user_email, 64).should match(/\?s=64/) + expect(gravatar_icon(user_email, 64)).to match(/\?s=64/) end - it "should use default size when size is wrong" do + it 'should use default size when size is wrong' do allow(self).to receive(:request).and_return(double(:ssl? => false)) - gravatar_icon(user_email, nil).should match(/\?s=40/) + expect(gravatar_icon(user_email, nil)).to match(/\?s=40/) end - it "should be case insensitive" do + it 'should be case insensitive' do allow(self).to receive(:request).and_return(double(:ssl? => false)) - gravatar_icon(user_email).should == gravatar_icon(user_email.upcase + " ") + expect(gravatar_icon(user_email)). + to eq(gravatar_icon(user_email.upcase + ' ')) end end - describe "grouped_options_refs" do + describe 'grouped_options_refs' do # Override Rails' grouped_options_for_select helper since HTML is harder to work with def grouped_options_for_select(options, *args) options @@ -130,100 +155,123 @@ describe ApplicationHelper do @project = create(:project) end - it "includes a list of branch names" do - options[0][0].should == 'Branches' - options[0][1].should include('master', 'feature') + it 'includes a list of branch names' do + expect(options[0][0]).to eq('Branches') + expect(options[0][1]).to include('master', 'feature') end - it "includes a list of tag names" do - options[1][0].should == 'Tags' - options[1][1].should include('v1.0.0','v1.1.0') + it 'includes a list of tag names' do + expect(options[1][0]).to eq('Tags') + expect(options[1][1]).to include('v1.0.0', 'v1.1.0') end - it "includes a specific commit ref if defined" do + it 'includes a specific commit ref if defined' do # Must be an instance variable @ref = '2ed06dc41dbb5936af845b87d79e05bbf24c73b8' - options[2][0].should == 'Commit' - options[2][1].should == [@ref] + expect(options[2][0]).to eq('Commit') + expect(options[2][1]).to eq([@ref]) end - it "sorts tags in a natural order" do + it 'sorts tags in a natural order' do # Stub repository.tag_names to make sure we get some valid testing data - expect(@project.repository).to receive(:tag_names).and_return(["v1.0.9", "v1.0.10", "v2.0", "v3.1.4.2", "v1.0.9a"]) + expect(@project.repository).to receive(:tag_names). + and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v2.0rc1¿', + 'v1.0.9a', 'v2.0-rc1', 'v2.0rc2']) - options[1][1].should == ["v3.1.4.2", "v2.0", "v1.0.10", "v1.0.9a", "v1.0.9"] + expect(options[1][1]). + to eq(['v3.1.4.2', 'v2.0', 'v2.0rc2', 'v2.0rc1¿', 'v2.0-rc1', 'v1.0.10', + 'v1.0.9', 'v1.0.9a']) end end - describe "user_color_scheme_class" do - context "with current_user is nil" do - it "should return a string" do - allow(self).to receive(:current_user).and_return(nil) - user_color_scheme_class.should be_kind_of(String) - end - end - - context "with a current_user" do - (1..5).each do |color_scheme_id| - context "with color_scheme_id == #{color_scheme_id}" do - it "should return a string" do - current_user = double(:color_scheme_id => color_scheme_id) - allow(self).to receive(:current_user).and_return(current_user) - user_color_scheme_class.should be_kind_of(String) - end - end - end - end - end - - describe "simple_sanitize" do + describe 'simple_sanitize' do let(:a_tag) { '<a href="#">Foo</a>' } - it "allows the a tag" do - simple_sanitize(a_tag).should == a_tag + it 'allows the a tag' do + expect(simple_sanitize(a_tag)).to eq(a_tag) end - it "allows the span tag" do + it 'allows the span tag' do input = '<span class="foo">Bar</span>' - simple_sanitize(input).should == input + expect(simple_sanitize(input)).to eq(input) end - it "disallows other tags" do + it 'disallows other tags' do input = "<strike><b>#{a_tag}</b></strike>" - simple_sanitize(input).should == a_tag + expect(simple_sanitize(input)).to eq(a_tag) end end - describe "link_to" do + describe 'time_ago_with_tooltip' do + def element(*arguments) + Time.zone = 'UTC' + time = Time.zone.parse('2015-07-02 08:00') + element = time_ago_with_tooltip(time, *arguments) + + Nokogiri::HTML::DocumentFragment.parse(element).first_element_child + end + + it 'returns a time element' do + expect(element.name).to eq 'time' + end + + it 'includes the date string' do + expect(element.text).to eq '2015-07-02 08:00:00 UTC' + end + + it 'has a datetime attribute' do + expect(element.attr('datetime')).to eq '2015-07-02T08:00:00Z' + end + + it 'has a formatted title attribute' do + expect(element.attr('title')).to eq 'Jul 02, 2015 8:00am' + end - it "should not include rel=nofollow for internal links" do - expect(link_to("Home", root_path)).to eq("<a href=\"/\">Home</a>") + it 'includes a default js-timeago class' do + expect(element.attr('class')).to eq 'time_ago js-timeago' end - it "should include rel=nofollow for external links" do - expect(link_to("Example", "http://www.example.com")).to eq("<a href=\"http://www.example.com\" rel=\"nofollow\">Example</a>") + it 'accepts a custom html_class' do + expect(element(html_class: 'custom_class').attr('class')).to eq 'custom_class js-timeago' end - it "should include re=nofollow for external links and honor existing html_options" do - expect( - link_to("Example", "http://www.example.com", class: "toggle", data: {toggle: "dropdown"}) - ).to eq("<a class=\"toggle\" data-toggle=\"dropdown\" href=\"http://www.example.com\" rel=\"nofollow\">Example</a>") + it 'accepts a custom tooltip placement' do + expect(element(placement: 'bottom').attr('data-placement')).to eq 'bottom' end - it "should include rel=nofollow for external links and preserver other rel values" do - expect( - link_to("Example", "http://www.example.com", rel: "noreferrer") - ).to eq("<a href=\"http://www.example.com\" rel=\"noreferrer nofollow\">Example</a>") + it 're-initializes timeago Javascript' do + el = element.next_element + + expect(el.name).to eq 'script' + expect(el.text).to include "$('.js-timeago').timeago()" + end + + it 'allows the script tag to be excluded' do + expect(element(skip_js: true)).not_to include 'script' end end - describe 'markup_render' do + describe 'render_markup' do let(:content) { 'Noël' } it 'should preserve encoding' do - content.encoding.name.should == 'UTF-8' + expect(content.encoding.name).to eq('UTF-8') expect(render_markup('foo.rst', content).encoding.name).to eq('UTF-8') end + + it "should delegate to #markdown when file name corresponds to Markdown" do + expect(self).to receive(:gitlab_markdown?).with('foo.md').and_return(true) + expect(self).to receive(:markdown).and_return('NOEL') + + expect(render_markup('foo.md', content)).to eq('NOEL') + end + + it "should delegate to #asciidoc when file name corresponds to AsciiDoc" do + expect(self).to receive(:asciidoc?).with('foo.adoc').and_return(true) + expect(self).to receive(:asciidoc).and_return('NOEL') + + expect(render_markup('foo.adoc', content)).to eq('NOEL') + end end end diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb new file mode 100644 index 00000000000..e49e4e6d5d8 --- /dev/null +++ b/spec/helpers/blob_helper_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe BlobHelper do + describe 'highlight' do + let(:blob_name) { 'test.lisp' } + let(:no_context_content) { ":type \"assem\"))" } + let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } + let(:split_content) { blob_content.split("\n") } + + it 'should return plaintext for unknown lexer context' do + result = highlight(blob_name, no_context_content, nowrap: true, continue: false) + expect(result).to eq('<span id="LC1" class="line">:type "assem"))</span>') + end + + it 'should highlight single block' do + expected = %Q[<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span> +<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span>] + + expect(highlight(blob_name, blob_content, nowrap: true, continue: false)).to eq(expected) + end + + it 'should highlight continued blocks' do + # Both lines have LC1 as ID since formatter doesn't support continue at the moment + expected = [ + '<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>', + '<span id="LC1" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span>' + ] + + result = split_content.map{ |content| highlight(blob_name, content, nowrap: true, continue: true) } + expect(result).to eq(expected) + end + end +end diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb index 1338ce4873d..c7c6f45d144 100644 --- a/spec/helpers/broadcast_messages_helper_spec.rb +++ b/spec/helpers/broadcast_messages_helper_spec.rb @@ -2,19 +2,20 @@ require 'spec_helper' describe BroadcastMessagesHelper do describe 'broadcast_styling' do - let(:broadcast_message) { double(color: "", font: "") } + let(:broadcast_message) { double(color: '', font: '') } context "default style" do it "should have no style" do - broadcast_styling(broadcast_message).should match('') + expect(broadcast_styling(broadcast_message)).to eq '' end end - context "customiezd style" do - before { broadcast_message.stub(color: "#f2dede", font: "#b94a48") } + context "customized style" do + let(:broadcast_message) { double(color: "#f2dede", font: '#b94a48') } it "should have a customized style" do - broadcast_styling(broadcast_message).should match('background-color:#f2dede;color:#b94a48') + expect(broadcast_styling(broadcast_message)). + to match('background-color: #f2dede; color: #b94a48') end end end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index b07742a6ee2..7c96a74e581 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -4,64 +4,129 @@ describe DiffHelper do include RepoHelpers let(:project) { create(:project) } - let(:commit) { project.repository.commit(sample_commit.id) } - let(:diff) { commit.diffs.first } + let(:commit) { project.commit(sample_commit.id) } + let(:diffs) { commit.diffs } + let(:diff) { diffs.first } let(:diff_file) { Gitlab::Diff::File.new(diff) } describe 'diff_hard_limit_enabled?' do it 'should return true if param is provided' do - controller.stub(:params).and_return { { :force_show_diff => true } } - diff_hard_limit_enabled?.should be_true + allow(controller).to receive(:params) { { force_show_diff: true } } + expect(diff_hard_limit_enabled?).to be_truthy end it 'should return false if param is not provided' do - diff_hard_limit_enabled?.should be_false + expect(diff_hard_limit_enabled?).to be_falsey end end describe 'allowed_diff_size' do it 'should return hard limit for a diff if force diff is true' do - controller.stub(:params).and_return { { :force_show_diff => true } } - allowed_diff_size.should eq(1000) + allow(controller).to receive(:params) { { force_show_diff: true } } + expect(allowed_diff_size).to eq(1000) end it 'should return safe limit for a diff if force diff is false' do - allowed_diff_size.should eq(100) + expect(allowed_diff_size).to eq(100) + end + end + + describe 'allowed_diff_lines' do + it 'should return hard limit for number of lines in a diff if force diff is true' do + allow(controller).to receive(:params) { { force_show_diff: true } } + expect(allowed_diff_lines).to eq(50000) + end + + it 'should return safe limit for numbers of lines a diff if force diff is false' do + expect(allowed_diff_lines).to eq(5000) + end + end + + describe 'safe_diff_files' do + it 'should return all files from a commit that is smaller than safe limits' do + expect(safe_diff_files(diffs).length).to eq(2) + end + + it 'should return only the first file if the diff line count in the 2nd file takes the total beyond safe limits' do + allow(diffs[1].diff).to receive(:lines).and_return([""] * 4999) #simulate 4999 lines + expect(safe_diff_files(diffs).length).to eq(1) + end + + it 'should return all files from a commit that is beyond safe limit for numbers of lines if force diff is true' do + allow(controller).to receive(:params) { { force_show_diff: true } } + allow(diffs[1].diff).to receive(:lines).and_return([""] * 4999) #simulate 4999 lines + expect(safe_diff_files(diffs).length).to eq(2) + end + + it 'should return only the first file if the diff line count in the 2nd file takes the total beyond hard limits' do + allow(controller).to receive(:params) { { force_show_diff: true } } + allow(diffs[1].diff).to receive(:lines).and_return([""] * 49999) #simulate 49999 lines + expect(safe_diff_files(diffs).length).to eq(1) + end + + it 'should return only a safe number of file diffs if a commit touches more files than the safe limits' do + large_diffs = diffs * 100 #simulate 200 diffs + expect(safe_diff_files(large_diffs).length).to eq(100) + end + + it 'should return all file diffs if a commit touches more files than the safe limits but force diff is true' do + allow(controller).to receive(:params) { { force_show_diff: true } } + large_diffs = diffs * 100 #simulate 200 diffs + expect(safe_diff_files(large_diffs).length).to eq(200) + end + + it 'should return a limited file diffs if a commit touches more files than the hard limits and force diff is true' do + allow(controller).to receive(:params) { { force_show_diff: true } } + very_large_diffs = diffs * 1000 #simulate 2000 diffs + expect(safe_diff_files(very_large_diffs).length).to eq(1000) end end describe 'parallel_diff' do it 'should return an array of arrays containing the parsed diff' do - parallel_diff(diff_file, 0).should match_array(parallel_diff_result_array) + expect(parallel_diff(diff_file, 0)). + to match_array(parallel_diff_result_array) end end describe 'generate_line_code' do it 'should generate correct line code' do - generate_line_code(diff_file.file_path, diff_file.diff_lines.first).should == '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6' + expect(generate_line_code(diff_file.file_path, diff_file.diff_lines.first)). + to eq('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6') end end describe 'unfold_bottom_class' do it 'should return empty string when bottom line shouldnt be unfolded' do - unfold_bottom_class(false).should == '' + expect(unfold_bottom_class(false)).to eq('') end it 'should return js class when bottom lines should be unfolded' do - unfold_bottom_class(true).should == 'js-unfold-bottom' + expect(unfold_bottom_class(true)).to eq('js-unfold-bottom') + end + end + + describe 'unfold_class' do + it 'returns empty on false' do + expect(unfold_class(false)).to eq('') + end + + it 'returns a class on true' do + expect(unfold_class(true)).to eq('unfold js-unfold') end end describe 'diff_line_content' do it 'should return non breaking space when line is empty' do - diff_line_content(nil).should eq(" ") + expect(diff_line_content(nil)).to eq(' ') end it 'should return the line itself' do - diff_line_content(diff_file.diff_lines.first.text).should eq("@@ -6,12 +6,18 @@ module Popen") - diff_line_content(diff_file.diff_lines.first.type).should eq("match") - diff_line_content(diff_file.diff_lines.first.new_pos).should eq(6) + expect(diff_line_content(diff_file.diff_lines.first.text)). + to eq('@@ -6,12 +6,18 @@ module Popen') + expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match') + expect(diff_line_content(diff_file.diff_lines.first.new_pos)).to eq(6) end end diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb new file mode 100644 index 00000000000..7a3e38d7e63 --- /dev/null +++ b/spec/helpers/emails_helper_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe EmailsHelper do + describe 'password_reset_token_valid_time' do + def validate_time_string(time_limit, expected_string) + Devise.reset_password_within = time_limit + expect(password_reset_token_valid_time).to eq(expected_string) + end + + context 'when time limit is less than 2 hours' do + it 'should display the time in hours using a singular unit' do + validate_time_string(1.hour, '1 hour') + end + end + + context 'when time limit is 2 or more hours' do + it 'should display the time in hours using a plural unit' do + validate_time_string(2.hours, '2 hours') + end + end + + context 'when time limit contains fractions of an hour' do + it 'should round down to the nearest hour' do + validate_time_string(96.minutes, '1 hour') + end + end + + context 'when time limit is 24 or more hours' do + it 'should display the time in days using a singular unit' do + validate_time_string(24.hours, '1 day') + end + end + + context 'when time limit is 2 or more days' do + it 'should display the time in days using a plural unit' do + validate_time_string(2.days, '2 days') + end + end + + context 'when time limit contains fractions of a day' do + it 'should round down to the nearest day' do + validate_time_string(57.hours, '2 days') + end + end + end +end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index 4de54d291f2..b392371deb4 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -4,6 +4,8 @@ describe EventsHelper do include ApplicationHelper include GitlabMarkdownHelper + 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(event_note(input)).to match(input) @@ -26,7 +28,8 @@ describe EventsHelper do it 'should display the first line of a code block' do input = "```\nCode block\nwith two lines\n```" - expected = '<pre><code class="">Code block...</code></pre>' + expected = '<pre class="code highlight white plaintext"><code>' \ + 'Code block...</code></pre>' expect(event_note(input)).to match(expected) end @@ -49,4 +52,14 @@ describe EventsHelper do expect(event_note(input)).to match(link_url) expect(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 white 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>\n" \ + '</code></pre>' + expect(event_note(input)).to eq(expected) + end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 3c636b747d1..bbb434638ce 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -1,786 +1,136 @@ -require "spec_helper" +require 'spec_helper' describe GitlabMarkdownHelper do include ApplicationHelper - include IssuesHelper let!(:project) { create(:project) } - let(:empty_project) { create(:empty_project) } let(:user) { create(:user, username: 'gfm') } - let(:commit) { project.repository.commit } + let(:commit) { project.commit } 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(:member) { project.project_members.where(user_id: user).first } - def url_helper(image_name) - File.join(root_url, 'assets', image_name) - end + # Helper expects a current_user method. + let(:current_user) { user } before do # Helper expects a @project instance variable @project = project - @ref = 'markdown' - @repository = project.repository end describe "#gfm" do - it "should return unaltered text if project is nil" do - actual = "Testing references: ##{issue.iid}" - - gfm(actual).should_not == actual - - @project = nil - gfm(actual).should == actual - end - - it "should not alter non-references" do - actual = expected = "_Please_ *stop* 'helping' and all the other b*$#%' you do." - gfm(actual).should == expected - end - - it "should not touch HTML entities" do - @project.issues.stub(:where).with(id: '39').and_return([issue]) - actual = 'We'll accept good pull requests.' - gfm(actual).should == "We'll accept good pull requests." - end - it "should forward HTML options to links" do - gfm("Fixed in #{commit.id}", @project, class: 'foo'). - should have_selector('a.gfm.foo') - end - - describe "referencing a commit" do - let(:expected) { project_commit_path(project, commit) } - - it "should link using a full id" do - actual = "Reverts #{commit.id}" - gfm(actual).should match(expected) - end - - it "should link using a short id" do - actual = "Backported from #{commit.short_id}" - gfm(actual).should match(expected) - end - - it "should link with adjacent text" do - actual = "Reverted (see #{commit.id})" - gfm(actual).should match(expected) - end - - it "should keep whitespace intact" do - actual = "Changes #{commit.id} dramatically" - expected = /Changes <a.+>#{commit.id}<\/a> dramatically/ - gfm(actual).should match(expected) - end - - it "should not link with an invalid id" do - actual = expected = "What happened in #{commit.id.reverse}" - gfm(actual).should == expected - end - - it "should include a title attribute" do - actual = "Reverts #{commit.id}" - gfm(actual).should match(/title="#{commit.link_title}"/) - end - - it "should include standard gfm classes" do - actual = "Reverts #{commit.id}" - gfm(actual).should match(/class="\s?gfm gfm-commit\s?"/) - end - end - - describe "referencing a team member" do - let(:actual) { "@#{user.username} you are right." } - let(:expected) { user_path(user) } - - before do - project.team << [user, :master] - end - - it "should link using a simple name" do - gfm(actual).should match(expected) - end - - it "should link using a name with dots" do - user.update_attributes(name: "alphA.Beta") - gfm(actual).should match(expected) - end - - it "should link using name with underscores" do - user.update_attributes(name: "ping_pong_king") - gfm(actual).should match(expected) - end - - it "should link with adjacent text" do - actual = "Mail the admin (@#{user.username})" - gfm(actual).should match(expected) - end - - it "should keep whitespace intact" do - actual = "Yes, @#{user.username} is right." - expected = /Yes, <a.+>@#{user.username}<\/a> is right/ - gfm(actual).should match(expected) - end - - it "should not link with an invalid id" do - actual = expected = "@#{user.username.reverse} you are right." - gfm(actual).should == expected - end - - it "should include standard gfm classes" do - gfm(actual).should match(/class="\s?gfm gfm-team_member\s?"/) - end - end - - # Shared examples for referencing an object - # - # Expects the following attributes to be available in the example group: - # - # - object - The object itself - # - reference - The object reference string (e.g., #1234, $1234, !1234) - # - # Currently limited to Snippets, Issues and MergeRequests - shared_examples 'referenced object' do - let(:actual) { "Reference to #{reference}" } - let(:expected) { polymorphic_path([project, object]) } - - it "should link using a valid id" do - gfm(actual).should match(expected) - end - - it "should link with adjacent text" do - # Wrap the reference in parenthesis - gfm(actual.gsub(reference, "(#{reference})")).should match(expected) - - # Append some text to the end of the reference - gfm(actual.gsub(reference, "#{reference}, right?")).should match(expected) - end - - it "should keep whitespace intact" do - actual = "Referenced #{reference} already." - expected = /Referenced <a.+>[^\s]+<\/a> already/ - gfm(actual).should match(expected) - end - - it "should not link with an invalid id" do - # Modify the reference string so it's still parsed, but is invalid - reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2)) - gfm(actual).should == actual - end - - it "should include a title attribute" do - title = "#{object.class.to_s.titlecase}: #{object.title}" - gfm(actual).should match(/title="#{title}"/) - end - - it "should include standard gfm classes" do - css = object.class.to_s.underscore - gfm(actual).should match(/class="\s?gfm gfm-#{css}\s?"/) - end - end - - # Shared examples for referencing an object in a different project - # - # Expects the following attributes to be available in the example group: - # - # - object - The object itself - # - reference - The object reference string (e.g., #1234, $1234, !1234) - # - other_project - The project that owns the target object - # - # Currently limited to Snippets, Issues and MergeRequests - shared_examples 'cross-project referenced object' do - let(:project_path) { @other_project.path_with_namespace } - let(:full_reference) { "#{project_path}#{reference}" } - let(:actual) { "Reference to #{full_reference}" } - let(:expected) do - if object.is_a?(Commit) - project_commit_path(@other_project, object) - else - polymorphic_path([@other_project, object]) - end - end - - it 'should link using a valid id' do - gfm(actual).should match( - /#{expected}.*#{Regexp.escape(full_reference)}/ - ) - end - - it 'should link with adjacent text' do - # Wrap the reference in parenthesis - gfm(actual.gsub(full_reference, "(#{full_reference})")).should( - match(expected) - ) - - # Append some text to the end of the reference - gfm(actual.gsub(full_reference, "#{full_reference}, right?")).should( - match(expected) - ) - end - - it 'should keep whitespace intact' do - actual = "Referenced #{full_reference} already." - expected = /Referenced <a.+>[^\s]+<\/a> already/ - gfm(actual).should match(expected) - end - - it 'should not link with an invalid id' do - # Modify the reference string so it's still parsed, but is invalid - if object.is_a?(Commit) - reference.gsub!(/^(.).+$/, '\1' + '12345abcd') - else - reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2)) - end - gfm(actual).should == actual - end - - it 'should include a title attribute' do - if object.is_a?(Commit) - title = object.link_title - else - title = "#{object.class.to_s.titlecase}: #{object.title}" - end - gfm(actual).should match(/title="#{title}"/) - end - - it 'should include standard gfm classes' do - css = object.class.to_s.underscore - gfm(actual).should match(/class="\s?gfm gfm-#{css}\s?"/) - end - end - - describe "referencing an issue" do - let(:object) { issue } - let(:reference) { "##{issue.iid}" } - - include_examples 'referenced object' - end - - context 'cross-repo references' do - before(:all) do - @other_project = create(:project, :public) - @commit2 = @other_project.repository.commit - @issue2 = create(:issue, project: @other_project) - @merge_request2 = create(:merge_request, - source_project: @other_project, - target_project: @other_project) - end - - describe 'referencing an issue in another project' do - let(:object) { @issue2 } - let(:reference) { "##{@issue2.iid}" } - - include_examples 'cross-project referenced object' - end - - describe 'referencing an merge request in another project' do - let(:object) { @merge_request2 } - let(:reference) { "!#{@merge_request2.iid}" } - - include_examples 'cross-project referenced object' - end - - describe 'referencing a commit in another project' do - let(:object) { @commit2 } - let(:reference) { "@#{@commit2.id}" } - - include_examples 'cross-project referenced object' - end - end - - describe "referencing a Jira issue" do - let(:actual) { "Reference to JIRA-#{issue.iid}" } - let(:expected) { "http://jira.example/browse/JIRA-#{issue.iid}" } - let(:reference) { "JIRA-#{issue.iid}" } - - before do - issue_tracker_config = { "jira" => { "title" => "JIRA tracker", "issues_url" => "http://jira.example/browse/:id" } } - Gitlab.config.stub(:issues_tracker).and_return(issue_tracker_config) - @project.stub(:issues_tracker).and_return("jira") - @project.stub(:issues_tracker_id).and_return("JIRA") - end - - it "should link using a valid id" do - gfm(actual).should match(expected) - end - - it "should link with adjacent text" do - # Wrap the reference in parenthesis - gfm(actual.gsub(reference, "(#{reference})")).should match(expected) - - # Append some text to the end of the reference - gfm(actual.gsub(reference, "#{reference}, right?")).should match(expected) - end - - it "should keep whitespace intact" do - actual = "Referenced #{reference} already." - expected = /Referenced <a.+>[^\s]+<\/a> already/ - gfm(actual).should match(expected) - end - - it "should not link with an invalid id" do - # Modify the reference string so it's still parsed, but is invalid - invalid_reference = actual.gsub(/(\d+)$/, "r45") - gfm(invalid_reference).should == invalid_reference - end - - it "should include a title attribute" do - title = "Issue in JIRA tracker" - gfm(actual).should match(/title="#{title}"/) - end - - it "should include standard gfm classes" do - gfm(actual).should match(/class="\s?gfm gfm-issue\s?"/) - end - end - - describe "referencing a merge request" do - let(:object) { merge_request } - let(:reference) { "!#{merge_request.iid}" } - - include_examples 'referenced object' - end - - describe "referencing a snippet" do - let(:object) { snippet } - let(:reference) { "$#{snippet.id}" } - let(:actual) { "Reference to #{reference}" } - let(:expected) { project_snippet_path(project, object) } - - it "should link using a valid id" do - gfm(actual).should match(expected) - end - - it "should link with adjacent text" do - # Wrap the reference in parenthesis - gfm(actual.gsub(reference, "(#{reference})")).should match(expected) - - # Append some text to the end of the reference - gfm(actual.gsub(reference, "#{reference}, right?")).should match(expected) - end - - it "should keep whitespace intact" do - actual = "Referenced #{reference} already." - expected = /Referenced <a.+>[^\s]+<\/a> already/ - gfm(actual).should match(expected) - end - - it "should not link with an invalid id" do - # Modify the reference string so it's still parsed, but is invalid - reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2)) - gfm(actual).should == actual - end - - it "should include a title attribute" do - title = "Snippet: #{object.title}" - gfm(actual).should match(/title="#{title}"/) - end - - it "should include standard gfm classes" do - css = object.class.to_s.underscore - gfm(actual).should match(/class="\s?gfm gfm-snippet\s?"/) - end - + expect(gfm("Fixed in #{commit.id}", { project: @project }, class: 'foo')). + to have_selector('a.gfm.foo') end describe "referencing multiple objects" do - let(:actual) { "!#{merge_request.iid} -> #{commit.id} -> ##{issue.iid}" } + let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" } it "should link to the merge request" do - expected = project_merge_request_path(project, merge_request) - gfm(actual).should match(expected) + expected = namespace_project_merge_request_path(project.namespace, project, merge_request) + expect(gfm(actual)).to match(expected) end it "should link to the commit" do - expected = project_commit_path(project, commit) - gfm(actual).should match(expected) + expected = namespace_project_commit_path(project.namespace, project, commit) + expect(gfm(actual)).to match(expected) end it "should link to the issue" do - expected = project_issue_path(project, issue) - gfm(actual).should match(expected) - end - end - - describe "emoji" do - it "matches at the start of a string" do - gfm(":+1:").should match(/<img/) - end - - it "matches at the end of a string" do - gfm("This gets a :-1:").should match(/<img/) - end - - it "matches with adjacent text" do - gfm("+1 (:+1:)").should match(/<img/) - end - - it "has a title attribute" do - gfm(":-1:").should match(/title=":-1:"/) - end - - it "has an alt attribute" do - gfm(":-1:").should match(/alt=":-1:"/) - end - - it "has an emoji class" do - gfm(":+1:").should match('class="emoji"') - end - - it "sets height and width" do - actual = gfm(":+1:") - actual.should match(/width="20"/) - actual.should match(/height="20"/) - end - - it "keeps whitespace intact" do - gfm('This deserves a :+1: big time.'). - should match(/deserves a <img.+> big time/) - end - - it "ignores invalid emoji" do - gfm(":invalid-emoji:").should_not match(/<img/) - end - - it "should work independent of reference links (i.e. without @project being set)" do - @project = nil - gfm(":+1:").should match(/<img/) + expected = namespace_project_issue_path(project.namespace, project, issue) + expect(gfm(actual)).to match(expected) end end end - describe "#link_to_gfm" do - let(:commit_path) { project_commit_path(project, commit) } + describe '#link_to_gfm' do + let(:commit_path) { namespace_project_commit_path(project.namespace, project, commit) } let(:issues) { create_list(:issue, 2, project: project) } - it "should handle references nested in links with all the text" do - actual = link_to_gfm("This should finally fix ##{issues[0].iid} and ##{issues[1].iid} for real", commit_path) + it 'should handle references nested in links with all the text' do + actual = link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path) + doc = Nokogiri::HTML.parse(actual) - # Break the result into groups of links with their content, without - # closing tags - groups = actual.split("</a>") + # Make sure we didn't create invalid markup + expect(doc.errors).to be_empty # Leading commit link - groups[0].should match(/href="#{commit_path}"/) - groups[0].should match(/This should finally fix $/) + expect(doc.css('a')[0].attr('href')).to eq commit_path + expect(doc.css('a')[0].text).to eq 'This should finally fix ' # First issue link - groups[1].should match(/href="#{project_issue_url(project, issues[0])}"/) - groups[1].should match(/##{issues[0].iid}$/) + expect(doc.css('a')[1].attr('href')). + to eq namespace_project_issue_path(project.namespace, project, issues[0]) + expect(doc.css('a')[1].text).to eq issues[0].to_reference # Internal commit link - groups[2].should match(/href="#{commit_path}"/) - groups[2].should match(/ and /) + expect(doc.css('a')[2].attr('href')).to eq commit_path + expect(doc.css('a')[2].text).to eq ' and ' # Second issue link - groups[3].should match(/href="#{project_issue_url(project, issues[1])}"/) - groups[3].should match(/##{issues[1].iid}$/) + expect(doc.css('a')[3].attr('href')). + to eq namespace_project_issue_path(project.namespace, project, issues[1]) + expect(doc.css('a')[3].text).to eq issues[1].to_reference # Trailing commit link - groups[4].should match(/href="#{commit_path}"/) - groups[4].should match(/ for real$/) + expect(doc.css('a')[4].attr('href')).to eq commit_path + expect(doc.css('a')[4].text).to eq ' for real' end - it "should forward HTML options" do + it 'should forward HTML options' do actual = link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo') - actual.should have_selector 'a.gfm.gfm-commit.foo' - end - - it "escapes HTML passed in as the body" do - actual = "This is a <h1>test</h1> - see ##{issues[0].iid}" - link_to_gfm(actual, commit_path).should match('<h1>test</h1>') - end - end - - describe "#markdown" do - it "should handle references in paragraphs" do - actual = "\n\nLorem ipsum dolor sit amet. #{commit.id} Nam pulvinar sapien eget.\n" - expected = project_commit_path(project, commit) - markdown(actual).should match(expected) - end - - it "should handle references in headers" do - actual = "\n# Working around ##{issue.iid}\n## Apply !#{merge_request.iid}" - - markdown(actual, {no_header_anchors:true}).should match(%r{<h1[^<]*>Working around <a.+>##{issue.iid}</a></h1>}) - markdown(actual, {no_header_anchors:true}).should match(%r{<h2[^<]*>Apply <a.+>!#{merge_request.iid}</a></h2>}) - end - - it "should add ids and links to headers" do - # Test every rule except nested tags. - text = '..Ab_c-d. e..' - id = 'ab_c-d-e' - markdown("# #{text}").should match(%r{<h1 id="#{id}">#{text}<a href="[^"]*##{id}"></a></h1>}) - markdown("# #{text}", {no_header_anchors:true}).should == "<h1>#{text}</h1>" - - id = 'link-text' - markdown("# [link text](url) ").should match( - %r{<h1 id="#{id}"><a href="[^"]*url">link text</a> <img[^>]*><a href="[^"]*##{id}"></a></h1>} - ) - end - - it "should handle references in lists" do - project.team << [user, :master] - - actual = "\n* dark: ##{issue.iid}\n* light by @#{member.user.username}" - - markdown(actual).should match(%r{<li>dark: <a.+>##{issue.iid}</a></li>}) - markdown(actual).should match(%r{<li>light by <a.+>@#{member.user.username}</a></li>}) - end - - it "should not link the apostrophe to issue 39" do - project.team << [user, :master] - project.issues.stub(:where).with(iid: '39').and_return([issue]) - - actual = "Yes, it is @#{member.user.username}'s task." - expected = /Yes, it is <a.+>@#{member.user.username}<\/a>'s task/ - markdown(actual).should match(expected) - end - - it "should not link the apostrophe to issue 39 in code blocks" do - project.team << [user, :master] - project.issues.stub(:where).with(iid: '39').and_return([issue]) - - actual = "Yes, `it is @#{member.user.username}'s task.`" - expected = /Yes, <code>it is @gfm\'s task.<\/code>/ - markdown(actual).should match(expected) - end - - it "should handle references in <em>" do - actual = "Apply _!#{merge_request.iid}_ ASAP" - - markdown(actual).should match(%r{Apply <em><a.+>!#{merge_request.iid}</a></em>}) - end - - it "should handle tables" do - actual = %Q{| header 1 | header 2 | -| -------- | -------- | -| cell 1 | cell 2 | -| cell 3 | cell 4 |} - - markdown(actual).should match(/\A<table/) - end - - it "should leave code blocks untouched" do - helper.stub(:user_color_scheme_class).and_return(:white) + doc = Nokogiri::HTML.parse(actual) - target_html = "\n<div class=\"highlighted-data white\">\n <div class=\"highlight\">\n <pre><code class=\"\">some code from $#{snippet.id}\nhere too\n</code></pre>\n </div>\n</div>\n\n" - - helper.markdown("\n some code from $#{snippet.id}\n here too\n").should == target_html - helper.markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n").should == target_html - end - - it "should leave inline code untouched" do - markdown("\nDon't use `$#{snippet.id}` here.\n").should == - "<p>Don't use <code>$#{snippet.id}</code> here.</p>\n" - end - - it "should leave ref-like autolinks untouched" do - markdown("look at http://example.tld/#!#{merge_request.iid}").should == "<p>look at <a href=\"http://example.tld/#!#{merge_request.iid}\">http://example.tld/#!#{merge_request.iid}</a></p>\n" - end - - it "should leave ref-like href of 'manual' links untouched" do - markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})").should == "<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a class=\"gfm gfm-merge_request \" href=\"#{project_merge_request_url(project, merge_request)}\" title=\"Merge Request: #{merge_request.title}\">!#{merge_request.iid}</a><a href=\"http://example.tld/#!#{merge_request.iid}\"></a></p>\n" - end - - it "should leave ref-like src of images untouched" do - markdown("screen shot: ").should == "<p>screen shot: <img src=\"http://example.tld/#!#{merge_request.iid}\" alt=\"some image\"></p>\n" - end - - it "should generate absolute urls for refs" do - markdown("##{issue.iid}").should include(project_issue_url(project, issue)) - end - - it "should generate absolute urls for emoji" do - markdown(':smile:').should( - include(%(src="#{Gitlab.config.gitlab.url}/assets/emoji/smile.png)) - ) - end - - it "should generate absolute urls for emoji if relative url is present" do - Gitlab.config.gitlab.stub(:url).and_return('http://localhost/gitlab/root') - markdown(":smile:").should include("src=\"http://localhost/gitlab/root/assets/emoji/smile.png") - end - - it "should generate absolute urls for emoji if asset_host is present" do - Gitlab::Application.config.stub(:asset_host).and_return("https://cdn.example.com") - ActionView::Base.any_instance.stub_chain(:config, :asset_host).and_return("https://cdn.example.com") - markdown(":smile:").should include("src=\"https://cdn.example.com/assets/emoji/smile.png") - end - - - it "should handle relative urls for a file in master" do - actual = "[GitLab API doc](doc/api/README.md)\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n" - markdown(actual).should match(expected) - end - - it "should handle relative urls for a directory in master" do - actual = "[GitLab API doc](doc/api)\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/tree/#{@ref}/doc/api\">GitLab API doc</a></p>\n" - markdown(actual).should match(expected) - end - - it "should handle absolute urls" do - actual = "[GitLab](https://www.gitlab.com)\n" - expected = "<p><a href=\"https://www.gitlab.com\">GitLab</a></p>\n" - markdown(actual).should match(expected) - end - - it "should handle relative urls in reference links for a file in master" do - actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n" - markdown(actual).should match(expected) - end - - it "should handle relative urls in reference links for a directory in master" do - actual = "[GitLab API doc directory][GitLab readmes]\n [GitLab readmes]: doc/api/\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/tree/#{@ref}/doc/api\">GitLab API doc directory</a></p>\n" - markdown(actual).should match(expected) - end - - it "should not handle malformed relative urls in reference links for a file in master" do - actual = "[GitLab readme]: doc/api/README.md\n" - expected = "" - markdown(actual).should match(expected) + expect(doc.css('a')).to satisfy do |v| + # 'foo' gets added to all links + v.all? { |a| a.attr('class').match(/foo$/) } + end end - end - describe 'markdown for empty repository' do - before do - @project = empty_project - @repository = empty_project.repository + it "escapes HTML passed in as the body" do + actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}" + expect(link_to_gfm(actual, commit_path)). + to match('<h1>test</h1>') end - it "should not touch relative urls" do - actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n" - expected = "<p><a href=\"doc/api/README.md\">GitLab API doc</a></p>\n" - markdown(actual).should match(expected) + it 'ignores reference links when they are the entire body' do + text = issues[0].to_reference + act = link_to_gfm(text, '/foo') + expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>) end end - describe "#render_wiki_content" do + describe '#render_wiki_content' do before do @wiki = double('WikiPage') - @wiki.stub(:content).and_return('wiki content') + allow(@wiki).to receive(:content).and_return('wiki content') end it "should use GitLab Flavored Markdown for markdown files" do - @wiki.stub(:format).and_return(:markdown) + allow(@wiki).to receive(:format).and_return(:markdown) - helper.should_receive(:markdown).with('wiki content') + expect(helper).to receive(:markdown).with('wiki content') helper.render_wiki_content(@wiki) end - it "should use the Gollum renderer for all other file types" do - @wiki.stub(:format).and_return(:rdoc) - formatted_content_stub = double('formatted_content') - formatted_content_stub.should_receive(:html_safe) - @wiki.stub(:formatted_content).and_return(formatted_content_stub) + it "should use Asciidoctor for asciidoc files" do + allow(@wiki).to receive(:format).and_return(:asciidoc) - helper.render_wiki_content(@wiki) - end - end - - describe '#gfm_with_tasks' do - before(:all) do - @source_text_asterisk = <<EOT.gsub(/^\s{8}/, '') - * [ ] valid unchecked task - * [x] valid lowercase checked task - * [X] valid uppercase checked task - * [ ] valid unchecked nested task - * [x] valid checked nested task - - [ ] not an unchecked task - no list item - [x] not a checked task - no list item - - * [ ] not an unchecked task - too many spaces - * [x ] not a checked task - too many spaces - * [] not an unchecked task - no spaces - * Not a task [ ] - not at beginning -EOT - - @source_text_dash = <<EOT.gsub(/^\s{8}/, '') - - [ ] valid unchecked task - - [x] valid lowercase checked task - - [X] valid uppercase checked task - - [ ] valid unchecked nested task - - [x] valid checked nested task -EOT - end + expect(helper).to receive(:asciidoc).with('wiki content') - it 'should render checkboxes at beginning of asterisk list items' do - rendered_text = markdown(@source_text_asterisk, parse_tasks: true) - - expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/) - expect(rendered_text).to match( - /<input.*checkbox.*valid lowercase checked task/ - ) - expect(rendered_text).to match( - /<input.*checkbox.*valid uppercase checked task/ - ) - end - - it 'should render checkboxes at beginning of dash list items' do - rendered_text = markdown(@source_text_dash, parse_tasks: true) - - expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/) - expect(rendered_text).to match( - /<input.*checkbox.*valid lowercase checked task/ - ) - expect(rendered_text).to match( - /<input.*checkbox.*valid uppercase checked task/ - ) - end - - it 'should not be confused by whitespace before bullets' do - rendered_text_asterisk = markdown(@source_text_asterisk, - parse_tasks: true) - rendered_text_dash = markdown(@source_text_dash, parse_tasks: true) - - expect(rendered_text_asterisk).to match( - /<input.*checkbox.*valid unchecked nested task/ - ) - expect(rendered_text_asterisk).to match( - /<input.*checkbox.*valid checked nested task/ - ) - expect(rendered_text_dash).to match( - /<input.*checkbox.*valid unchecked nested task/ - ) - expect(rendered_text_dash).to match( - /<input.*checkbox.*valid checked nested task/ - ) - end - - it 'should not render checkboxes outside of list items' do - rendered_text = markdown(@source_text_asterisk, parse_tasks: true) - - expect(rendered_text).not_to match( - /<input.*checkbox.*not an unchecked task - no list item/ - ) - expect(rendered_text).not_to match( - /<input.*checkbox.*not a checked task - no list item/ - ) + helper.render_wiki_content(@wiki) end - it 'should not render checkboxes with invalid formatting' do - rendered_text = markdown(@source_text_asterisk, parse_tasks: true) + it "should use the Gollum renderer for all other file types" do + allow(@wiki).to receive(:format).and_return(:rdoc) + formatted_content_stub = double('formatted_content') + expect(formatted_content_stub).to receive(:html_safe) + allow(@wiki).to receive(:formatted_content).and_return(formatted_content_stub) - expect(rendered_text).not_to match( - /<input.*checkbox.*not an unchecked task - too many spaces/ - ) - expect(rendered_text).not_to match( - /<input.*checkbox.*not a checked task - too many spaces/ - ) - expect(rendered_text).not_to match( - /<input.*checkbox.*not an unchecked task - no spaces/ - ) - expect(rendered_text).not_to match( - /Not a task.*<input.*checkbox.*not at beginning/ - ) + helper.render_wiki_content(@wiki) end end end diff --git a/spec/helpers/groups_helper.rb b/spec/helpers/groups_helper.rb new file mode 100644 index 00000000000..3e99ab84ec9 --- /dev/null +++ b/spec/helpers/groups_helper.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe GroupsHelper do + describe 'group_icon' do + avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png') + + it 'should return an url for the avatar' do + group = create(:group) + group.avatar = File.open(avatar_file_path) + group.save! + expect(group_icon(group.path).to_s). + to match("/uploads/group/avatar/#{ group.id }/gitlab_logo.png") + end + + it 'should give default avatar_icon when no avatar is present' do + group = create(:group) + group.save! + expect(group_icon(group.path)).to match('group_avatar.png') + end + end +end diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb new file mode 100644 index 00000000000..c052981fe73 --- /dev/null +++ b/spec/helpers/icons_helper_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +describe IconsHelper do + describe 'file_type_icon_class' do + it 'returns folder class' do + expect(file_type_icon_class('folder', 0, 'folder_name')).to eq 'folder' + end + + it 'returns share class' do + expect(file_type_icon_class('file', '120000', 'link')).to eq 'share' + end + + it 'returns file-pdf-o class with .pdf' do + expect(file_type_icon_class('file', 0, 'filename.pdf')).to eq 'file-pdf-o' + end + + it 'returns file-image-o class with .jpg' do + expect(file_type_icon_class('file', 0, 'filename.jpg')).to eq 'file-image-o' + end + + it 'returns file-image-o class with .JPG' do + expect(file_type_icon_class('file', 0, 'filename.JPG')).to eq 'file-image-o' + end + + it 'returns file-image-o class with .png' do + expect(file_type_icon_class('file', 0, 'filename.png')).to eq 'file-image-o' + end + + it 'returns file-archive-o class with .tar' do + expect(file_type_icon_class('file', 0, 'filename.tar')).to eq 'file-archive-o' + end + + it 'returns file-archive-o class with .TAR' do + expect(file_type_icon_class('file', 0, 'filename.TAR')).to eq 'file-archive-o' + end + + it 'returns file-archive-o class with .tar.gz' do + expect(file_type_icon_class('file', 0, 'filename.tar.gz')).to eq 'file-archive-o' + end + + it 'returns file-audio-o class with .mp3' do + expect(file_type_icon_class('file', 0, 'filename.mp3')).to eq 'file-audio-o' + end + + it 'returns file-audio-o class with .MP3' do + expect(file_type_icon_class('file', 0, 'filename.MP3')).to eq 'file-audio-o' + end + + it 'returns file-audio-o class with .wav' do + expect(file_type_icon_class('file', 0, 'filename.wav')).to eq 'file-audio-o' + end + + it 'returns file-video-o class with .avi' do + expect(file_type_icon_class('file', 0, 'filename.avi')).to eq 'file-video-o' + end + + it 'returns file-video-o class with .AVI' do + expect(file_type_icon_class('file', 0, 'filename.AVI')).to eq 'file-video-o' + end + + it 'returns file-video-o class with .mp4' do + expect(file_type_icon_class('file', 0, 'filename.mp4')).to eq 'file-video-o' + end + + it 'returns file-word-o class with .doc' do + expect(file_type_icon_class('file', 0, 'filename.doc')).to eq 'file-word-o' + end + + it 'returns file-word-o class with .DOC' do + expect(file_type_icon_class('file', 0, 'filename.DOC')).to eq 'file-word-o' + end + + it 'returns file-word-o class with .docx' do + expect(file_type_icon_class('file', 0, 'filename.docx')).to eq 'file-word-o' + end + + it 'returns file-excel-o class with .xls' do + expect(file_type_icon_class('file', 0, 'filename.xls')).to eq 'file-excel-o' + end + + it 'returns file-excel-o class with .XLS' do + expect(file_type_icon_class('file', 0, 'filename.XLS')).to eq 'file-excel-o' + end + + it 'returns file-excel-o class with .xlsx' do + expect(file_type_icon_class('file', 0, 'filename.xlsx')).to eq 'file-excel-o' + end + + it 'returns file-excel-o class with .ppt' do + expect(file_type_icon_class('file', 0, 'filename.ppt')).to eq 'file-powerpoint-o' + end + + it 'returns file-excel-o class with .PPT' do + expect(file_type_icon_class('file', 0, 'filename.PPT')).to eq 'file-powerpoint-o' + end + + it 'returns file-excel-o class with .pptx' do + expect(file_type_icon_class('file', 0, 'filename.pptx')).to eq 'file-powerpoint-o' + end + + it 'returns file-text-o class with .unknow' do + expect(file_type_icon_class('file', 0, 'filename.unknow')).to eq 'file-text-o' + end + + it 'returns file-text-o class with no extension' do + expect(file_type_icon_class('file', 0, 'CHANGELOG')).to eq 'file-text-o' + end + end +end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 9c95bc044f3..c08ddb4cae1 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -5,133 +5,114 @@ describe IssuesHelper do let(:issue) { create :issue, project: project } let(:ext_project) { create :redmine_project } - describe :title_for_issue do - it "should return issue title if used internal tracker" do - @project = project - title_for_issue(issue.iid).should eq issue.title - end - - it "should always return empty string if used external tracker" do - @project = ext_project - title_for_issue(rand(100)).should eq "" - end - - it "should always return empty string if project nil" do - @project = nil - - title_for_issue(rand(100)).should eq "" - end - end - - describe :url_for_project_issues do - let(:project_url) { Gitlab.config.issues_tracker.redmine.project_url} + describe "url_for_project_issues" do + let(:project_url) { ext_project.external_issue_tracker.project_url } let(:ext_expected) do project_url.gsub(':project_id', ext_project.id.to_s) .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s) end - let(:int_expected) { polymorphic_path([project]) } + let(:int_expected) { polymorphic_path([@project.namespace, project]) } it "should return internal path if used internal tracker" do @project = project - url_for_project_issues.should match(int_expected) + expect(url_for_project_issues).to match(int_expected) end it "should return path to external tracker" do @project = ext_project - url_for_project_issues.should match(ext_expected) + expect(url_for_project_issues).to match(ext_expected) end it "should return empty string if project nil" do @project = nil - url_for_project_issues.should eq "" + expect(url_for_project_issues).to eq "" end describe "when external tracker was enabled and then config removed" do before do @project = ext_project - Gitlab.config.stub(:issues_tracker).and_return(nil) + allow(Gitlab.config).to receive(:issues_tracker).and_return(nil) end - it "should return path to internal tracker" do - url_for_project_issues.should match(polymorphic_path([@project])) + it "should return path to external tracker" do + expect(url_for_project_issues).to match(ext_expected) end end end - describe :url_for_issue do - let(:issue_id) { 3 } - let(:issues_url) { Gitlab.config.issues_tracker.redmine.issues_url} + describe "url_for_issue" do + let(:issues_url) { ext_project.external_issue_tracker.issues_url} let(:ext_expected) do - issues_url.gsub(':id', issue_id.to_s) + issues_url.gsub(':id', issue.iid.to_s) .gsub(':project_id', ext_project.id.to_s) .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s) end - let(:int_expected) { polymorphic_path([project, issue]) } + let(:int_expected) { polymorphic_path([@project.namespace, project, issue]) } it "should return internal path if used internal tracker" do @project = project - url_for_issue(issue.iid).should match(int_expected) + expect(url_for_issue(issue.iid)).to match(int_expected) end it "should return path to external tracker" do @project = ext_project - url_for_issue(issue_id).should match(ext_expected) + expect(url_for_issue(issue.iid)).to match(ext_expected) end it "should return empty string if project nil" do @project = nil - url_for_issue(issue.iid).should eq "" + expect(url_for_issue(issue.iid)).to eq "" end describe "when external tracker was enabled and then config removed" do before do @project = ext_project - Gitlab.config.stub(:issues_tracker).and_return(nil) + allow(Gitlab.config).to receive(:issues_tracker).and_return(nil) end - it "should return internal path" do - url_for_issue(issue.iid).should match(polymorphic_path([@project, issue])) + it "should return external path" do + expect(url_for_issue(issue.iid)).to match(ext_expected) end end end - describe :url_for_new_issue do - let(:issues_url) { Gitlab.config.issues_tracker.redmine.new_issue_url} + describe '#url_for_new_issue' do + let(:issues_url) { ext_project.external_issue_tracker.new_issue_url } let(:ext_expected) do issues_url.gsub(':project_id', ext_project.id.to_s) .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s) end - let(:int_expected) { new_project_issue_path(project) } + let(:int_expected) { new_namespace_project_issue_path(project.namespace, project) } it "should return internal path if used internal tracker" do @project = project - url_for_new_issue.should match(int_expected) + expect(url_for_new_issue).to match(int_expected) end it "should return path to external tracker" do @project = ext_project - url_for_new_issue.should match(ext_expected) + expect(url_for_new_issue).to match(ext_expected) end it "should return empty string if project nil" do @project = nil - url_for_new_issue.should eq "" + expect(url_for_new_issue).to eq "" end describe "when external tracker was enabled and then config removed" do before do @project = ext_project - Gitlab.config.stub(:issues_tracker).and_return(nil) + allow(Gitlab.config).to receive(:issues_tracker).and_return(nil) end it "should return internal path" do - url_for_new_issue.should match(new_project_issue_path(@project)) + expect(url_for_new_issue).to match(ext_expected) end end end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 1e64a201942..0c8d06b7059 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -1,6 +1,70 @@ require 'spec_helper' describe LabelsHelper do - it { expect(text_color_for_bg('#EEEEEE')).to eq('#333') } - it { expect(text_color_for_bg('#222E2E')).to eq('#FFF') } + describe 'link_to_label' do + let(:project) { create(:empty_project) } + let(:label) { create(:label, project: project) } + + context 'with @project set' do + before do + @project = project + end + + it 'uses the instance variable' do + expect(label).not_to receive(:project) + link_to_label(label) + end + end + + context 'without @project set' do + it "uses the label's project" do + expect(label).to receive(:project).and_return(project) + link_to_label(label) + end + end + + context 'with a named project argument' do + it 'uses the provided project' do + arg = double('project') + expect(arg).to receive(:namespace).and_return('foo') + expect(arg).to receive(:to_param).and_return('foo') + + link_to_label(label, project: arg) + end + + it 'takes precedence over other types' do + @project = project + expect(@project).not_to receive(:namespace) + expect(label).not_to receive(:project) + + arg = double('project', namespace: 'foo', to_param: 'foo') + link_to_label(label, project: arg) + end + end + + context 'with block' do + it 'passes the block to link_to' do + link = link_to_label(label) { 'Foo' } + expect(link).to match('Foo') + end + end + + context 'without block' do + it 'uses render_colored_label as the link content' do + expect(self).to receive(:render_colored_label). + with(label).and_return('Foo') + expect(link_to_label(label)).to match('Foo') + end + end + end + + describe 'text_color_for_bg' do + it 'uses light text on dark backgrounds' do + expect(text_color_for_bg('#222E2E')).to eq('#FFFFFF') + end + + it 'uses dark text on light backgrounds' do + expect(text_color_for_bg('#EEEEEE')).to eq('#333333') + end + end end diff --git a/spec/helpers/merge_requests_helper.rb b/spec/helpers/merge_requests_helper.rb index 5a317c4886b..5262d644048 100644 --- a/spec/helpers/merge_requests_helper.rb +++ b/spec/helpers/merge_requests_helper.rb @@ -7,6 +7,6 @@ describe MergeRequestsHelper do [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)] end - it { should eq('#1, #2, and #3') } + it { is_expected.to eq('#1, #2, and #3') } end end diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb new file mode 100644 index 00000000000..e4d18d8bfc6 --- /dev/null +++ b/spec/helpers/nav_helper_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +# Specs in this file have access to a helper object that includes +# the NavHelper. For example: +# +# describe NavHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +describe NavHelper do + describe '#nav_menu_collapsed?' do + it 'returns true when the nav is collapsed in the cookie' do + helper.request.cookies[:collapsed_nav] = 'true' + expect(helper.nav_menu_collapsed?).to eq true + end + + it 'returns false when the nav is not collapsed in the cookie' do + helper.request.cookies[:collapsed_nav] = 'false' + expect(helper.nav_menu_collapsed?).to eq false + end + end +end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index 31ecdacf28e..f1aba4cfdf3 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -5,31 +5,31 @@ describe NotificationsHelper do let(:notification) { double(disabled?: false, participating?: false, watch?: false) } context "disabled notification" do - before { notification.stub(disabled?: true) } + before { allow(notification).to receive(:disabled?).and_return(true) } it "has a red icon" do - notification_icon(notification).should match('class="fa fa-volume-off ns-mute"') + expect(notification_icon(notification)).to match('class="fa fa-volume-off ns-mute"') end end context "participating notification" do - before { notification.stub(participating?: true) } + before { allow(notification).to receive(:participating?).and_return(true) } it "has a blue icon" do - notification_icon(notification).should match('class="fa fa-volume-down ns-part"') + expect(notification_icon(notification)).to match('class="fa fa-volume-down ns-part"') end end context "watched notification" do - before { notification.stub(watch?: true) } + before { allow(notification).to receive(:watch?).and_return(true) } it "has a green icon" do - notification_icon(notification).should match('class="fa fa-volume-up ns-watch"') + expect(notification_icon(notification)).to match('class="fa fa-volume-up ns-watch"') end end it "has a blue icon" do - notification_icon(notification).should match('class="fa fa-circle-o ns-default"') + expect(notification_icon(notification)).to match('class="fa fa-circle-o ns-default"') end end end diff --git a/spec/helpers/oauth_helper_spec.rb b/spec/helpers/oauth_helper_spec.rb new file mode 100644 index 00000000000..088c342fa13 --- /dev/null +++ b/spec/helpers/oauth_helper_spec.rb @@ -0,0 +1,20 @@ +require "spec_helper" + +describe OauthHelper do + describe "additional_providers" do + it 'returns all enabled providers' do + allow(helper).to receive(:enabled_oauth_providers) { [:twitter, :github] } + expect(helper.additional_providers).to include(*[:twitter, :github]) + end + + it 'does not return ldap provider' do + allow(helper).to receive(:enabled_oauth_providers) { [:twitter, :ldapmain] } + expect(helper.additional_providers).to include(:twitter) + end + + it 'returns empty array' do + allow(helper).to receive(:enabled_oauth_providers) { [] } + expect(helper.additional_providers).to eq([]) + end + end +end
\ No newline at end of file diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb new file mode 100644 index 00000000000..920de8c4325 --- /dev/null +++ b/spec/helpers/preferences_helper_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe PreferencesHelper do + describe 'user_application_theme' do + context 'with a user' do + it "returns user's theme's css_class" do + user = double('user', theme_id: 3) + allow(self).to receive(:current_user).and_return(user) + expect(user_application_theme).to eq 'ui_green' + end + + it 'returns the default when id is invalid' do + user = double('user', theme_id: Gitlab::Themes::THEMES.size + 5) + + allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(2) + allow(self).to receive(:current_user).and_return(user) + + expect(user_application_theme).to eq 'ui_charcoal' + end + end + + context 'without a user' do + before do + allow(self).to receive(:current_user).and_return(nil) + end + + it 'returns the default theme' do + expect(user_application_theme).to eq Gitlab::Themes.default.css_class + end + end + end + + describe 'dashboard_choices' do + it 'raises an exception when defined choices may be missing' do + expect(User).to receive(:dashboards).and_return(foo: 'foo') + expect { dashboard_choices }.to raise_error(RuntimeError) + end + + it 'raises an exception when defined choices may be using the wrong key' do + expect(User).to receive(:dashboards).and_return(foo: 'foo', bar: 'bar') + expect { dashboard_choices }.to raise_error(KeyError) + end + + it 'provides better option descriptions' do + expect(dashboard_choices).to match_array [ + ['Your Projects (default)', 'projects'], + ['Starred Projects', 'stars'] + ] + end + end + + describe 'user_color_scheme_class' do + context 'with current_user is nil' do + it 'should return a string' do + allow(self).to receive(:current_user).and_return(nil) + expect(user_color_scheme_class).to be_kind_of(String) + end + end + + context 'with a current_user' do + (1..5).each do |color_scheme_id| + context "with color_scheme_id == #{color_scheme_id}" do + it 'should return a string' do + current_user = double(:color_scheme_id => color_scheme_id) + allow(self).to receive(:current_user).and_return(current_user) + expect(user_color_scheme_class).to be_kind_of(String) + end + end + end + end + end +end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 114058e3095..0f78725e3d9 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -1,23 +1,11 @@ require 'spec_helper' describe ProjectsHelper do - describe '#project_issues_trackers' do - it "returns the correct issues trackers available" do - project_issues_trackers.should == - "<option value=\"redmine\">Redmine</option>\n" \ - "<option value=\"gitlab\">GitLab</option>" - end - - it "returns the correct issues trackers available with current tracker 'gitlab' selected" do - project_issues_trackers('gitlab').should == - "<option value=\"redmine\">Redmine</option>\n" \ - "<option selected=\"selected\" value=\"gitlab\">GitLab</option>" - end - - it "returns the correct issues trackers available with current tracker 'redmine' selected" do - project_issues_trackers('redmine').should == - "<option selected=\"selected\" value=\"redmine\">Redmine</option>\n" \ - "<option value=\"gitlab\">GitLab</option>" + describe "#project_status_css_class" do + it "returns appropriate class" do + expect(project_status_css_class("started")).to eq("active") + expect(project_status_css_class("failed")).to eq("danger") + expect(project_status_css_class("finished")).to eq("success") end end end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 733f2754727..b327f4f911a 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -13,7 +13,7 @@ describe SearchHelper do end it "it returns nil" do - search_autocomplete_opts("q").should be_nil + expect(search_autocomplete_opts("q")).to be_nil end end @@ -25,29 +25,29 @@ describe SearchHelper do end it "includes Help sections" do - search_autocomplete_opts("hel").size.should == 9 + expect(search_autocomplete_opts("hel").size).to eq(9) end it "includes default sections" do - search_autocomplete_opts("adm").size.should == 1 + expect(search_autocomplete_opts("adm").size).to eq(1) end it "includes the user's groups" do create(:group).add_owner(user) - search_autocomplete_opts("gro").size.should == 1 + expect(search_autocomplete_opts("gro").size).to eq(1) end it "includes the user's projects" do project = create(:project, namespace: create(:namespace, owner: user)) - search_autocomplete_opts(project.name).size.should == 1 + expect(search_autocomplete_opts(project.name).size).to eq(1) end context "with a current project" do before { @project = create(:project) } it "includes project-specific sections" do - search_autocomplete_opts("Files").size.should == 1 - search_autocomplete_opts("Commits").size.should == 1 + expect(search_autocomplete_opts("Files").size).to eq(1) + expect(search_autocomplete_opts("Commits").size).to eq(1) end end end diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index 41c9f038c26..a7abf9d3839 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe SubmoduleHelper do + include RepoHelpers + describe 'submodule links' do let(:submodule_item) { double(id: 'hash', path: 'rack') } let(:config) { Gitlab.config.gitlab } @@ -12,108 +14,148 @@ describe SubmoduleHelper do context 'submodule on self' do before do - Gitlab.config.gitlab.stub(protocol: 'http') # set this just to be sure + allow(Gitlab.config.gitlab).to receive(:protocol).and_return('http') # set this just to be sure end it 'should detect ssh on standard port' do - Gitlab.config.gitlab_shell.stub(ssh_port: 22) # set this just to be sure - Gitlab.config.gitlab_shell.stub(ssh_path_prefix: Settings.send(:build_gitlab_shell_ssh_path_prefix)) + allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(22) # set this just to be sure + allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix)) stub_url([ config.user, '@', config.host, ':gitlab-org/gitlab-ce.git' ].join('')) - submodule_links(submodule_item).should == [ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ] + expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) end it 'should detect ssh on non-standard port' do - Gitlab.config.gitlab_shell.stub(ssh_port: 2222) - Gitlab.config.gitlab_shell.stub(ssh_path_prefix: Settings.send(:build_gitlab_shell_ssh_path_prefix)) + allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(2222) + allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix)) stub_url([ 'ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git' ].join('')) - submodule_links(submodule_item).should == [ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ] + expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) end it 'should detect http on standard port' do - Gitlab.config.gitlab.stub(port: 80) - Gitlab.config.gitlab.stub(url: Settings.send(:build_gitlab_url)) + allow(Gitlab.config.gitlab).to receive(:port).and_return(80) + allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) stub_url([ 'http://', config.host, '/gitlab-org/gitlab-ce.git' ].join('')) - submodule_links(submodule_item).should == [ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ] + expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) end it 'should detect http on non-standard port' do - Gitlab.config.gitlab.stub(port: 3000) - Gitlab.config.gitlab.stub(url: Settings.send(:build_gitlab_url)) + allow(Gitlab.config.gitlab).to receive(:port).and_return(3000) + allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) stub_url([ 'http://', config.host, ':3000/gitlab-org/gitlab-ce.git' ].join('')) - submodule_links(submodule_item).should == [ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ] + expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) end it 'should work with relative_url_root' do - Gitlab.config.gitlab.stub(port: 80) # set this just to be sure - Gitlab.config.gitlab.stub(relative_url_root: '/gitlab/root') - Gitlab.config.gitlab.stub(url: Settings.send(:build_gitlab_url)) + allow(Gitlab.config.gitlab).to receive(:port).and_return(80) # set this just to be sure + allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root') + allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) stub_url([ 'http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git' ].join('')) - submodule_links(submodule_item).should == [ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ] + expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) end end context 'submodule on github.com' do it 'should detect ssh' do stub_url('git@github.com:gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ] + expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ]) end it 'should detect http' do stub_url('http://github.com/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ] + expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ]) end it 'should detect https' do stub_url('https://github.com/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ] + expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ]) end it 'should return original with non-standard url' do stub_url('http://github.com/gitlab-org/gitlab-ce') - submodule_links(submodule_item).should == [ repo.submodule_url_for, nil ] + expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) stub_url('http://github.com/another/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ repo.submodule_url_for, nil ] + expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) end end context 'submodule on gitlab.com' do it 'should detect ssh' do stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ] + expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ]) end it 'should detect http' do stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ] + expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ]) end it 'should detect https' do stub_url('https://gitlab.com/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ] + expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ]) end it 'should return original with non-standard url' do stub_url('http://gitlab.com/gitlab-org/gitlab-ce') - submodule_links(submodule_item).should == [ repo.submodule_url_for, nil ] + expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) stub_url('http://gitlab.com/another/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ repo.submodule_url_for, nil ] + expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) end end context 'submodule on unsupported' do it 'should return original' do stub_url('http://mygitserver.com/gitlab-org/gitlab-ce') - submodule_links(submodule_item).should == [ repo.submodule_url_for, nil ] + expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) stub_url('http://mygitserver.com/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ repo.submodule_url_for, nil ] + expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) + end + end + + context 'submodules with relative links' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:commit_id) { sample_commit[:id] } + + before do + self.instance_variable_set(:@project, project) + end + + it 'one level down' do + result = relative_self_links('../test.git', commit_id) + expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) + end + + it 'two levels down' do + result = relative_self_links('../../test.git', commit_id) + expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) + end + + it 'one level down with namespace and repo' do + result = relative_self_links('../foobar/test.git', commit_id) + expect(result).to eq(["/foobar/test", "/foobar/test/tree/#{commit_id}"]) + end + + it 'two levels down with namespace and repo' do + result = relative_self_links('../foobar/baz/test.git', commit_id) + expect(result).to eq(["/baz/test", "/baz/test/tree/#{commit_id}"]) + end + + context 'personal project' do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + + it 'one level down with personal project' do + result = relative_self_links('../test.git', commit_id) + expect(result).to eq(["/#{user.username}/test", "/#{user.username}/test/tree/#{commit_id}"]) + end end end end def stub_url(url) - repo.stub(submodule_url_for: url) + allow(repo).to receive(:submodule_url_for).and_return(url) end end diff --git a/spec/helpers/tab_helper_spec.rb b/spec/helpers/tab_helper_spec.rb index fa8a3f554f7..fc0ceecfbe7 100644 --- a/spec/helpers/tab_helper_spec.rb +++ b/spec/helpers/tab_helper_spec.rb @@ -5,40 +5,40 @@ describe TabHelper do describe 'nav_link' do before do - controller.stub(:controller_name).and_return('foo') + allow(controller).to receive(:controller_name).and_return('foo') allow(self).to receive(:action_name).and_return('foo') end it "captures block output" do - nav_link { "Testing Blocks" }.should match(/Testing Blocks/) + expect(nav_link { "Testing Blocks" }).to match(/Testing Blocks/) end it "performs checks on the current controller" do - nav_link(controller: :foo).should match(/<li class="active">/) - nav_link(controller: :bar).should_not match(/active/) - nav_link(controller: [:foo, :bar]).should match(/active/) + expect(nav_link(controller: :foo)).to match(/<li class="active">/) + expect(nav_link(controller: :bar)).not_to match(/active/) + expect(nav_link(controller: [:foo, :bar])).to match(/active/) end it "performs checks on the current action" do - nav_link(action: :foo).should match(/<li class="active">/) - nav_link(action: :bar).should_not match(/active/) - nav_link(action: [:foo, :bar]).should match(/active/) + expect(nav_link(action: :foo)).to match(/<li class="active">/) + expect(nav_link(action: :bar)).not_to match(/active/) + expect(nav_link(action: [:foo, :bar])).to match(/active/) end it "performs checks on both controller and action when both are present" do - nav_link(controller: :bar, action: :foo).should_not match(/active/) - nav_link(controller: :foo, action: :bar).should_not match(/active/) - nav_link(controller: :foo, action: :foo).should match(/active/) + expect(nav_link(controller: :bar, action: :foo)).not_to match(/active/) + expect(nav_link(controller: :foo, action: :bar)).not_to match(/active/) + expect(nav_link(controller: :foo, action: :foo)).to match(/active/) end it "accepts a path shorthand" do - nav_link(path: 'foo#bar').should_not match(/active/) - nav_link(path: 'foo#foo').should match(/active/) + expect(nav_link(path: 'foo#bar')).not_to match(/active/) + expect(nav_link(path: 'foo#foo')).to match(/active/) end it "passes extra html options to the list element" do - nav_link(action: :foo, html_options: {class: 'home'}).should match(/<li class="home active">/) - nav_link(html_options: {class: 'active'}).should match(/<li class="active">/) + expect(nav_link(action: :foo, html_options: {class: 'home'})).to match(/<li class="home active">/) + expect(nav_link(html_options: {class: 'active'})).to match(/<li class="active">/) end end end diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb new file mode 100644 index 00000000000..2013b3e4c2a --- /dev/null +++ b/spec/helpers/tree_helper_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe TreeHelper do + describe 'flatten_tree' do + let(:project) { create(:project) } + + before { + @repository = project.repository + @commit = project.commit("e56497bb") + } + + context "on a directory containing more than one file/directory" do + let(:tree_item) { double(name: "files", path: "files") } + + it "should return the directory name" do + expect(flatten_tree(tree_item)).to match('files') + end + end + + context "on a directory containing only one directory" do + let(:tree_item) { double(name: "foo", path: "foo") } + + it "should return the flattened path" do + expect(flatten_tree(tree_item)).to match('foo/bar') + end + end + end +end diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb new file mode 100644 index 00000000000..3840e64981f --- /dev/null +++ b/spec/helpers/visibility_level_helper_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe VisibilityLevelHelper do + include Haml::Helpers + + before :all do + init_haml_helpers + end + + let(:project) { create(:project) } + + describe 'visibility_level_description' do + shared_examples 'a visibility level description' do + let(:desc) do + visibility_level_description(Gitlab::VisibilityLevel::PRIVATE, + form_model) + end + + let(:expected_class) do + class_name = case form_model.class.name + when 'String' + form_model + else + form_model.class.name + end + + class_name.match(/(project|snippet)$/i)[0] + end + + it 'should refer to the correct class' do + expect(desc).to match(/#{expected_class}/i) + end + end + + context 'form_model argument is a String' do + context 'model object is a personal snippet' do + it_behaves_like 'a visibility level description' do + let(:form_model) { 'PersonalSnippet' } + end + end + + context 'model object is a project snippet' do + it_behaves_like 'a visibility level description' do + let(:form_model) { 'ProjectSnippet' } + end + end + + context 'model object is a project' do + it_behaves_like 'a visibility level description' do + let(:form_model) { 'Project' } + end + end + end + + context 'form_model argument is a model object' do + context 'model object is a personal snippet' do + it_behaves_like 'a visibility level description' do + let(:form_model) { create(:personal_snippet) } + end + end + + context 'model object is a project snippet' do + it_behaves_like 'a visibility level description' do + let(:form_model) { create(:project_snippet, project: project) } + end + end + + context 'model object is a project' do + it_behaves_like 'a visibility level description' do + let(:form_model) { project } + end + end + end + end +end diff --git a/spec/javascripts/extensions/array_spec.js.coffee b/spec/javascripts/extensions/array_spec.js.coffee new file mode 100644 index 00000000000..4ceac619422 --- /dev/null +++ b/spec/javascripts/extensions/array_spec.js.coffee @@ -0,0 +1,12 @@ +#= require extensions/array + +describe 'Array extensions', -> + describe 'first', -> + it 'returns the first item', -> + arr = [0, 1, 2, 3, 4, 5] + expect(arr.first()).toBe(0) + + describe 'last', -> + it 'returns the last item', -> + arr = [0, 1, 2, 3, 4, 5] + expect(arr.last()).toBe(5) diff --git a/spec/javascripts/extensions/jquery_spec.js.coffee b/spec/javascripts/extensions/jquery_spec.js.coffee new file mode 100644 index 00000000000..b10e16b7d01 --- /dev/null +++ b/spec/javascripts/extensions/jquery_spec.js.coffee @@ -0,0 +1,34 @@ +#= require extensions/jquery + +describe 'jQuery extensions', -> + describe 'disable', -> + beforeEach -> + fixture.set '<input type="text" />' + + it 'adds the disabled attribute', -> + $input = $('input').first() + + $input.disable() + expect($input).toHaveAttr('disabled', 'disabled') + + it 'adds the disabled class', -> + $input = $('input').first() + + $input.disable() + expect($input).toHaveClass('disabled') + + describe 'enable', -> + beforeEach -> + fixture.set '<input type="text" disabled="disabled" class="disabled" />' + + it 'removes the disabled attribute', -> + $input = $('input').first() + + $input.enable() + expect($input).not.toHaveAttr('disabled') + + it 'removes the disabled class', -> + $input = $('input').first() + + $input.enable() + expect($input).not.toHaveClass('disabled') diff --git a/spec/javascripts/fixtures/issuable.html.haml b/spec/javascripts/fixtures/issuable.html.haml new file mode 100644 index 00000000000..42ab4aa68b1 --- /dev/null +++ b/spec/javascripts/fixtures/issuable.html.haml @@ -0,0 +1,2 @@ +%form.js-main-target-form + %textarea#note_note diff --git a/spec/javascripts/fixtures/issue_note.html.haml b/spec/javascripts/fixtures/issue_note.html.haml new file mode 100644 index 00000000000..0aecc7334fd --- /dev/null +++ b/spec/javascripts/fixtures/issue_note.html.haml @@ -0,0 +1,12 @@ +%ul + %li.note + .js-task-list-container + .note-text + %ul.task-list + %li.task-list-item + %input.task-list-item-checkbox{type: 'checkbox'} + Task List Item + .note-edit-form + %form + %textarea.js-task-list-field + \- [ ] Task List Item diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml new file mode 100644 index 00000000000..db5abe0cae3 --- /dev/null +++ b/spec/javascripts/fixtures/issues_show.html.haml @@ -0,0 +1,13 @@ +%a.btn-close + +.issue-details + .description.js-task-list-container + .wiki + %ul.task-list + %li.task-list-item + %input.task-list-item-checkbox{type: 'checkbox'} + Task List Item + %textarea.js-task-list-field + \- [ ] Task List Item + +%form.js-issue-update{action: '/foo'} diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml new file mode 100644 index 00000000000..15ad1d8968f --- /dev/null +++ b/spec/javascripts/fixtures/line_highlighter.html.haml @@ -0,0 +1,9 @@ +#tree-content-holder + .file-content + .line-numbers + - 1.upto(25) do |i| + %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}= i + %pre.code.highlight + %code + - 1.upto(25) do |i| + %span.line{id: "LC#{i}"}= "Line #{i}" diff --git a/spec/javascripts/fixtures/merge_request_tabs.html.haml b/spec/javascripts/fixtures/merge_request_tabs.html.haml new file mode 100644 index 00000000000..7624a713948 --- /dev/null +++ b/spec/javascripts/fixtures/merge_request_tabs.html.haml @@ -0,0 +1,22 @@ +%ul.nav.nav-tabs.merge-request-tabs + %li.notes-tab + %a{href: '/foo/bar/merge_requests/1', data: {target: '#notes', action: 'notes', toggle: 'tab'}} + Discussion + %li.commits-tab + %a{href: '/foo/bar/merge_requests/1/commits', data: {target: '#commits', action: 'commits', toggle: 'tab'}} + Commits + %li.diffs-tab + %a{href: '/foo/bar/merge_requests/1/diffs', data: {target: '#diffs', action: 'diffs', toggle: 'tab'}} + Diffs + +.tab-content + #notes.notes.tab-pane + Notes Content + #commits.commits.tab-pane + Commits Content + #diffs.diffs.tab-pane + Diffs Content + +.mr-loading-status + .loading + Loading Animation diff --git a/spec/javascripts/fixtures/merge_requests_show.html.haml b/spec/javascripts/fixtures/merge_requests_show.html.haml new file mode 100644 index 00000000000..c4329b8f94a --- /dev/null +++ b/spec/javascripts/fixtures/merge_requests_show.html.haml @@ -0,0 +1,13 @@ +%a.btn-close + +.merge-request-details + .description.js-task-list-container + .wiki + %ul.task-list + %li.task-list-item + %input.task-list-item-checkbox{type: 'checkbox'} + Task List Item + %textarea.js-task-list-field + \- [ ] Task List Item + +%form.js-merge-request-update{action: '/foo'} diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml new file mode 100644 index 00000000000..e867e4de2b9 --- /dev/null +++ b/spec/javascripts/fixtures/zen_mode.html.haml @@ -0,0 +1,9 @@ +.zennable + %input#zen-toggle-comment.zen-toggle-comment{ tabindex: '-1', type: 'checkbox' } + .zen-backdrop + %textarea#note_note.js-gfm-input.markdown-area{placeholder: 'Leave a comment'} + %a.zen-enter-link{tabindex: '-1'} + %i.fa.fa-expand + Edit in fullscreen + %a.zen-leave-link + %i.fa.fa-compress diff --git a/spec/javascripts/helpers/.gitkeep b/spec/javascripts/helpers/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 --- a/spec/javascripts/helpers/.gitkeep +++ /dev/null diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee new file mode 100644 index 00000000000..268e4c68c31 --- /dev/null +++ b/spec/javascripts/issue_spec.js.coffee @@ -0,0 +1,22 @@ +#= require issue + +describe 'Issue', -> + describe 'task lists', -> + fixture.preload('issues_show.html') + + beforeEach -> + fixture.load('issues_show.html') + @issue = new Issue() + + it 'modifies the Markdown field', -> + spyOn(jQuery, 'ajax').and.stub() + $('input[type=checkbox]').attr('checked', true).trigger('change') + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') + + it 'submits an ajax request on tasklist:changed', -> + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PATCH') + expect(req.url).toBe('/foo') + expect(req.data.issue.description).not.toBe(null) + + $('.js-task-list-field').trigger('tasklist:changed') diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee new file mode 100644 index 00000000000..14fa487ff7f --- /dev/null +++ b/spec/javascripts/line_highlighter_spec.js.coffee @@ -0,0 +1,150 @@ +#= require line_highlighter + +describe 'LineHighlighter', -> + fixture.preload('line_highlighter.html') + + clickLine = (number, eventData = {}) -> + if $.isEmptyObject(eventData) + $("#L#{number}").mousedown().click() + else + e = $.Event 'mousedown', eventData + $("#L#{number}").trigger(e).click() + + beforeEach -> + fixture.load('line_highlighter.html') + @class = new LineHighlighter() + @css = @class.highlightClass + @spies = { + __setLocationHash__: spyOn(@class, '__setLocationHash__').and.callFake -> + } + + describe 'behavior', -> + it 'highlights one line given in the URL hash', -> + new LineHighlighter('#L13') + expect($('#LC13')).toHaveClass(@css) + + it 'highlights a range of lines given in the URL hash', -> + new LineHighlighter('#L5-25') + expect($(".#{@css}").length).toBe(21) + expect($("#LC#{line}")).toHaveClass(@css) for line in [5..25] + + it 'scrolls to the first highlighted line on initial load', -> + spy = spyOn($, 'scrollTo') + new LineHighlighter('#L5-25') + expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()) + + it 'discards click events', -> + spy = spyOnEvent('a[data-line-number]', 'click') + clickLine(13) + expect(spy).toHaveBeenPrevented() + + it 'handles garbage input from the hash', -> + func = -> new LineHighlighter('#tree-content-holder') + expect(func).not.toThrow() + + describe '#clickHandler', -> + it 'discards the mousedown event', -> + spy = spyOnEvent('a[data-line-number]', 'mousedown') + clickLine(13) + expect(spy).toHaveBeenPrevented() + + describe 'without shiftKey', -> + it 'highlights one line when clicked', -> + clickLine(13) + expect($('#LC13')).toHaveClass(@css) + + it 'unhighlights previously highlighted lines', -> + clickLine(13) + clickLine(20) + + expect($('#LC13')).not.toHaveClass(@css) + expect($('#LC20')).toHaveClass(@css) + + it 'sets the hash', -> + spy = spyOn(@class, 'setHash').and.callThrough() + clickLine(13) + expect(spy).toHaveBeenCalledWith(13) + + describe 'with shiftKey', -> + it 'sets the hash', -> + spy = spyOn(@class, 'setHash').and.callThrough() + clickLine(13) + clickLine(20, shiftKey: true) + expect(spy).toHaveBeenCalledWith(13) + expect(spy).toHaveBeenCalledWith(13, 20) + + describe 'without existing highlight', -> + it 'highlights the clicked line', -> + clickLine(13, shiftKey: true) + expect($('#LC13')).toHaveClass(@css) + expect($(".#{@css}").length).toBe(1) + + it 'sets the hash', -> + spy = spyOn(@class, 'setHash') + clickLine(13, shiftKey: true) + expect(spy).toHaveBeenCalledWith(13) + + describe 'with existing single-line highlight', -> + it 'uses existing line as last line when target is lesser', -> + clickLine(20) + clickLine(15, shiftKey: true) + expect($(".#{@css}").length).toBe(6) + expect($("#LC#{line}")).toHaveClass(@css) for line in [15..20] + + it 'uses existing line as first line when target is greater', -> + clickLine(5) + clickLine(10, shiftKey: true) + expect($(".#{@css}").length).toBe(6) + expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10] + + describe 'with existing multi-line highlight', -> + beforeEach -> + clickLine(10, shiftKey: true) + clickLine(13, shiftKey: true) + + it 'uses target as first line when it is less than existing first line', -> + clickLine(5, shiftKey: true) + expect($(".#{@css}").length).toBe(6) + expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10] + + it 'uses target as last line when it is greater than existing first line', -> + clickLine(15, shiftKey: true) + expect($(".#{@css}").length).toBe(6) + expect($("#LC#{line}")).toHaveClass(@css) for line in [10..15] + + describe '#hashToRange', -> + beforeEach -> + @subject = @class.hashToRange + + it 'extracts a single line number from the hash', -> + expect(@subject('#L5')).toEqual([5, null]) + + it 'extracts a range of line numbers from the hash', -> + expect(@subject('#L5-15')).toEqual([5, 15]) + + it 'returns [null, null] when the hash is not a line number', -> + expect(@subject('#foo')).toEqual([null, null]) + + describe '#highlightLine', -> + beforeEach -> + @subject = @class.highlightLine + + it 'highlights the specified line', -> + @subject(13) + expect($('#LC13')).toHaveClass(@css) + + it 'accepts a String-based number', -> + @subject('13') + expect($('#LC13')).toHaveClass(@css) + + describe '#setHash', -> + beforeEach -> + @subject = @class.setHash + + it 'sets the location hash for a single line', -> + @subject(5) + expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5') + + it 'sets the location hash for a range', -> + @subject(5, 15) + expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15') diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee new file mode 100644 index 00000000000..a4735af0343 --- /dev/null +++ b/spec/javascripts/merge_request_spec.js.coffee @@ -0,0 +1,25 @@ +#= require merge_request + +window.disableButtonIfEmptyField = -> null + +describe 'MergeRequest', -> + describe 'task lists', -> + fixture.preload('merge_requests_show.html') + + beforeEach -> + fixture.load('merge_requests_show.html') + @merge = new MergeRequest({}) + + it 'modifies the Markdown field', -> + spyOn(jQuery, 'ajax').and.stub() + + $('input[type=checkbox]').attr('checked', true).trigger('change') + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') + + it 'submits an ajax request on tasklist:changed', -> + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PATCH') + expect(req.url).toBe('/foo') + expect(req.data.merge_request.description).not.toBe(null) + + $('.js-task-list-field').trigger('tasklist:changed') diff --git a/spec/javascripts/merge_request_tabs_spec.js.coffee b/spec/javascripts/merge_request_tabs_spec.js.coffee new file mode 100644 index 00000000000..6cc96fb68a0 --- /dev/null +++ b/spec/javascripts/merge_request_tabs_spec.js.coffee @@ -0,0 +1,82 @@ +#= require merge_request_tabs + +describe 'MergeRequestTabs', -> + stubLocation = (stubs) -> + defaults = {pathname: '', search: '', hash: ''} + $.extend(defaults, stubs) + + fixture.preload('merge_request_tabs.html') + + beforeEach -> + @class = new MergeRequestTabs() + @spies = { + ajax: spyOn($, 'ajax').and.callFake -> + history: spyOn(history, 'replaceState').and.callFake -> + } + + describe '#activateTab', -> + beforeEach -> + fixture.load('merge_request_tabs.html') + @subject = @class.activateTab + + it 'shows the first tab when action is show', -> + @subject('show') + expect($('#notes')).toHaveClass('active') + + it 'shows the notes tab when action is notes', -> + @subject('notes') + expect($('#notes')).toHaveClass('active') + + it 'shows the commits tab when action is commits', -> + @subject('commits') + expect($('#commits')).toHaveClass('active') + + it 'shows the diffs tab when action is diffs', -> + @subject('diffs') + expect($('#diffs')).toHaveClass('active') + + describe '#setCurrentAction', -> + beforeEach -> + @subject = @class.setCurrentAction + + it 'changes from commits', -> + @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits') + + expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') + expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs') + + it 'changes from diffs', -> + @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs') + + expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') + expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') + + it 'changes from notes', -> + @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1') + + expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs') + expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') + + it 'includes search parameters and hash string', -> + @class._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/diffs' + search: '?view=parallel' + hash: '#L15-35' + }) + + expect(@subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35') + + it 'replaces the current history state', -> + @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1') + new_state = @subject('commits') + + expect(@spies.history).toHaveBeenCalledWith( + {turbolinks: true, url: new_state}, + document.title, + new_state + ) + + it 'treats "show" like "notes"', -> + @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits') + + expect(@subject('show')).toBe('/foo/bar/merge_requests/1') diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee new file mode 100644 index 00000000000..050b6e362c6 --- /dev/null +++ b/spec/javascripts/notes_spec.js.coffee @@ -0,0 +1,25 @@ +#= require notes + +window.gon = {} +window.disableButtonIfEmptyField = -> null + +describe 'Notes', -> + describe 'task lists', -> + fixture.preload('issue_note.html') + + beforeEach -> + fixture.load('issue_note.html') + $('form').on 'submit', (e) -> e.preventDefault() + + @notes = new Notes() + + it 'modifies the Markdown field', -> + $('input[type=checkbox]').attr('checked', true).trigger('change') + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') + + it 'submits the form on tasklist:changed', -> + submitted = false + $('form').on 'submit', (e) -> submitted = true; e.preventDefault() + + $('.js-task-list-field').trigger('tasklist:changed') + expect(submitted).toBe(true) diff --git a/spec/javascripts/shortcuts_issuable_spec.js.coffee b/spec/javascripts/shortcuts_issuable_spec.js.coffee new file mode 100644 index 00000000000..a01ad7140dd --- /dev/null +++ b/spec/javascripts/shortcuts_issuable_spec.js.coffee @@ -0,0 +1,82 @@ +#= require shortcuts_issuable + +describe 'ShortcutsIssuable', -> + fixture.preload('issuable.html') + + beforeEach -> + fixture.load('issuable.html') + @shortcut = new ShortcutsIssuable() + + describe '#replyWithSelectedText', -> + # Stub window.getSelection to return the provided String. + stubSelection = (text) -> + window.getSelection = -> text + + beforeEach -> + @selector = 'form.js-main-target-form textarea#note_note' + + describe 'with empty selection', -> + it 'does nothing', -> + stubSelection('') + @shortcut.replyWithSelectedText() + expect($(@selector).val()).toBe('') + + describe 'with any selection', -> + beforeEach -> + stubSelection('Selected text.') + + it 'leaves existing input intact', -> + $(@selector).val('This text was already here.') + expect($(@selector).val()).toBe('This text was already here.') + + @shortcut.replyWithSelectedText() + expect($(@selector).val()). + toBe("This text was already here.\n> Selected text.\n\n") + + it 'triggers `input`', -> + triggered = false + $(@selector).on 'input', -> triggered = true + @shortcut.replyWithSelectedText() + + expect(triggered).toBe(true) + + it 'triggers `focus`', -> + focused = false + $(@selector).on 'focus', -> focused = true + @shortcut.replyWithSelectedText() + + expect(focused).toBe(true) + + describe 'with a one-line selection', -> + it 'quotes the selection', -> + stubSelection('This text has been selected.') + + @shortcut.replyWithSelectedText() + + expect($(@selector).val()). + toBe("> This text has been selected.\n\n") + + describe 'with a multi-line selection', -> + it 'quotes the selected lines as a group', -> + stubSelection( + """ + Selected line one. + + Selected line two. + Selected line three. + + """ + ) + + @shortcut.replyWithSelectedText() + + expect($(@selector).val()). + toBe( + """ + > Selected line one. + > Selected line two. + > Selected line three. + + + """ + ) diff --git a/spec/javascripts/spec_helper.coffee b/spec/javascripts/spec_helper.coffee new file mode 100644 index 00000000000..47b41dd2c81 --- /dev/null +++ b/spec/javascripts/spec_helper.coffee @@ -0,0 +1,46 @@ +# PhantomJS (Teaspoons default driver) doesn't have support for +# Function.prototype.bind, which has caused confusion. Use this polyfill to +# avoid the confusion. + +#= require support/bind-poly + +# You can require your own javascript files here. By default this will include +# everything in application, however you may get better load performance if you +# require the specific files that are being used in the spec that tests them. + +#= require jquery +#= require bootstrap +#= require underscore + +# Teaspoon includes some support files, but you can use anything from your own +# support path too. + +# require support/jasmine-jquery-1.7.0 +# require support/jasmine-jquery-2.0.0 +#= require support/jasmine-jquery-2.1.0 +# require support/sinon +# require support/your-support-file + +# Deferring execution + +# If you're using CommonJS, RequireJS or some other asynchronous library you can +# defer execution. Call Teaspoon.execute() after everything has been loaded. +# Simple example of a timeout: + +# Teaspoon.defer = true +# setTimeout(Teaspoon.execute, 1000) + +# Matching files + +# By default Teaspoon will look for files that match +# _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path +# and it'll be included in the default suite automatically. If you want to +# customize suites, check out the configuration in teaspoon_env.rb + +# Manifest + +# If you'd rather require your spec files manually (to control order for +# instance) you can disable the suite matcher in the configuration and use this +# file as a manifest. + +# For more information: http://github.com/modeset/teaspoon diff --git a/spec/javascripts/stat_graph_contributors_graph_spec.js b/spec/javascripts/stat_graph_contributors_graph_spec.js index 1090cb7f620..78d39f1b428 100644 --- a/spec/javascripts/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/stat_graph_contributors_graph_spec.js @@ -1,3 +1,5 @@ +//= require stat_graph_contributors_graph + describe("ContributorsGraph", function () { describe("#set_x_domain", function () { it("set the x_domain", function () { diff --git a/spec/javascripts/stat_graph_contributors_util_spec.js b/spec/javascripts/stat_graph_contributors_util_spec.js index 9c1b588861d..dbafe782b77 100644 --- a/spec/javascripts/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/stat_graph_contributors_util_spec.js @@ -1,3 +1,5 @@ +//= require stat_graph_contributors_util + describe("ContributorsStatGraphUtil", function () { describe("#parse_log", function () { @@ -116,9 +118,11 @@ describe("ContributorsStatGraphUtil", function () { describe("#add_author", function () { it("adds an author field to the collection", function () { var fake_author = { author_name: "Author", author_email: 'fake@email.com' } - var fake_collection = {} - ContributorsStatGraphUtil.add_author(fake_author, fake_collection) - expect(fake_collection[fake_author.author_name].author_name).toEqual("Author") + var fake_author_collection = {} + var fake_email_collection = {} + ContributorsStatGraphUtil.add_author(fake_author, fake_author_collection, fake_email_collection) + expect(fake_author_collection[fake_author.author_name].author_name).toEqual("Author") + expect(fake_email_collection[fake_author.author_email].author_name).toEqual("Author") }) }) diff --git a/spec/javascripts/stat_graph_spec.js b/spec/javascripts/stat_graph_spec.js index b589af34610..4c652910cd6 100644 --- a/spec/javascripts/stat_graph_spec.js +++ b/spec/javascripts/stat_graph_spec.js @@ -1,3 +1,5 @@ +//= require stat_graph + describe("StatGraph", function () { describe("#get_log", function () { diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml deleted file mode 100644 index 9bfa261a356..00000000000 --- a/spec/javascripts/support/jasmine.yml +++ /dev/null @@ -1,76 +0,0 @@ -# src_files -# -# Return an array of filepaths relative to src_dir to include before jasmine specs. -# Default: [] -# -# EXAMPLE: -# -# src_files: -# - lib/source1.js -# - lib/source2.js -# - dist/**/*.js -# -src_files: - - assets/application.js - -# stylesheets -# -# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs. -# Default: [] -# -# EXAMPLE: -# -# stylesheets: -# - css/style.css -# - stylesheets/*.css -# -stylesheets: - - stylesheets/**/*.css - -# helpers -# -# Return an array of filepaths relative to spec_dir to include before jasmine specs. -# Default: ["helpers/**/*.js"] -# -# EXAMPLE: -# -# helpers: -# - helpers/**/*.js -# -helpers: - - helpers/**/*.js - -# spec_files -# -# Return an array of filepaths relative to spec_dir to include. -# Default: ["**/*[sS]pec.js"] -# -# EXAMPLE: -# -# spec_files: -# - **/*[sS]pec.js -# -spec_files: - - '**/*[sS]pec.js' - -# src_dir -# -# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank. -# Default: project root -# -# EXAMPLE: -# -# src_dir: public -# -src_dir: - -# spec_dir -# -# Spec directory path. Your spec_files must be returned relative to this path. -# Default: spec/javascripts -# -# EXAMPLE: -# -# spec_dir: spec/javascripts -# -spec_dir: spec/javascripts diff --git a/spec/javascripts/support/jasmine_helper.rb b/spec/javascripts/support/jasmine_helper.rb deleted file mode 100644 index b4919802afe..00000000000 --- a/spec/javascripts/support/jasmine_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -#Use this file to set/override Jasmine configuration options -#You can remove it if you don't need it. -#This file is loaded *after* jasmine.yml is interpreted. -# -#Example: using a different boot file. -#Jasmine.configure do |config| -# config.boot_dir = '/absolute/path/to/boot_dir' -# config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] } -#end -# - diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee new file mode 100644 index 00000000000..1f4ea58ad48 --- /dev/null +++ b/spec/javascripts/zen_mode_spec.js.coffee @@ -0,0 +1,52 @@ +#= require zen_mode + +describe 'ZenMode', -> + fixture.preload('zen_mode.html') + + beforeEach -> + fixture.load('zen_mode.html') + + # Stub Dropzone.forElement(...).enable() + spyOn(Dropzone, 'forElement').and.callFake -> + enable: -> true + + @zen = new ZenMode() + + # Set this manually because we can't actually scroll the window + @zen.scroll_position = 456 + + # Ohmmmmmmm + enterZen = -> + $('.zen-toggle-comment').prop('checked', true).trigger('change') + + # Wh- what was that?! + exitZen = -> + $('.zen-toggle-comment').prop('checked', false).trigger('change') + + describe 'on enter', -> + it 'pauses Mousetrap', -> + spyOn(Mousetrap, 'pause') + enterZen() + expect(Mousetrap.pause).toHaveBeenCalled() + + describe 'in use', -> + beforeEach -> + enterZen() + + it 'exits on Escape', -> + $(document).trigger(jQuery.Event('keydown', {keyCode: 27})) + expect($('.zen-toggle-comment').prop('checked')).toBe(false) + + describe 'on exit', -> + beforeEach -> + enterZen() + + it 'unpauses Mousetrap', -> + spyOn(Mousetrap, 'unpause') + exitZen() + expect(Mousetrap.unpause).toHaveBeenCalled() + + it 'restores the scroll position', -> + spyOn(@zen, 'restoreScroll') + exitZen() + expect(@zen.restoreScroll).toHaveBeenCalledWith(456) diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb index 8bf6ee2ed50..06d5450688b 100644 --- a/spec/lib/disable_email_interceptor_spec.rb +++ b/spec/lib/disable_email_interceptor_spec.rb @@ -6,7 +6,7 @@ describe DisableEmailInterceptor do end it 'should not send emails' do - Gitlab.config.gitlab.stub(:email_enabled).and_return(false) + allow(Gitlab.config.gitlab).to receive(:email_enabled).and_return(false) expect { deliver_mail }.not_to change(ActionMailer::Base.deliveries, :count) diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index 7b3818ea5c8..8e05e44defc 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -2,56 +2,77 @@ require 'spec_helper' describe ExtractsPath do include ExtractsPath + include RepoHelpers + include Rails.application.routes.url_helpers let(:project) { double('project') } before do @project = project - project.stub(repository: double(ref_names: ['master', 'foo/bar/baz', 'v1.0.0', 'v2.0.0'])) - project.stub(path_with_namespace: 'gitlab/gitlab-ci') + + repo = double(ref_names: ['master', 'foo/bar/baz', 'v1.0.0', 'v2.0.0']) + allow(project).to receive(:repository).and_return(repo) + allow(project).to receive(:path_with_namespace). + and_return('gitlab/gitlab-ci') + end + + describe '#assign_ref' do + let(:ref) { sample_commit[:id] } + let(:params) { {path: sample_commit[:line_code_path], ref: ref} } + + before do + @project = create(:project) + end + + it "log tree path should have no escape sequences" do + assign_ref_vars + expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb") + end end describe '#extract_ref' do it "returns an empty pair when no @project is set" do @project = nil - extract_ref('master/CHANGELOG').should == ['', ''] + expect(extract_ref('master/CHANGELOG')).to eq(['', '']) end context "without a path" do it "extracts a valid branch" do - extract_ref('master').should == ['master', ''] + expect(extract_ref('master')).to eq(['master', '']) end it "extracts a valid tag" do - extract_ref('v2.0.0').should == ['v2.0.0', ''] + expect(extract_ref('v2.0.0')).to eq(['v2.0.0', '']) end it "extracts a valid commit ref without a path" do - extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062').should == + expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062')).to eq( ['f4b14494ef6abf3d144c28e4af0c20143383e062', ''] + ) end it "falls back to a primitive split for an invalid ref" do - extract_ref('stable').should == ['stable', ''] + expect(extract_ref('stable')).to eq(['stable', '']) end end context "with a path" do it "extracts a valid branch" do - extract_ref('foo/bar/baz/CHANGELOG').should == ['foo/bar/baz', 'CHANGELOG'] + expect(extract_ref('foo/bar/baz/CHANGELOG')).to eq(['foo/bar/baz', 'CHANGELOG']) end it "extracts a valid tag" do - extract_ref('v2.0.0/CHANGELOG').should == ['v2.0.0', 'CHANGELOG'] + expect(extract_ref('v2.0.0/CHANGELOG')).to eq(['v2.0.0', 'CHANGELOG']) end it "extracts a valid commit SHA" do - extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG').should == + expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG')).to eq( ['f4b14494ef6abf3d144c28e4af0c20143383e062', 'CHANGELOG'] + ) end it "falls back to a primitive split for an invalid ref" do - extract_ref('stable/CHANGELOG').should == ['stable', 'CHANGELOG'] + expect(extract_ref('stable/CHANGELOG')).to eq(['stable', 'CHANGELOG']) end end end diff --git a/spec/lib/file_size_validator_spec.rb b/spec/lib/file_size_validator_spec.rb new file mode 100644 index 00000000000..5c89c854714 --- /dev/null +++ b/spec/lib/file_size_validator_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe 'Gitlab::FileSizeValidatorSpec' do + let(:validator) { FileSizeValidator.new(options) } + let(:attachment) { AttachmentUploader.new } + let(:note) { create(:note) } + + describe 'options uses an integer' do + let(:options) { { maximum: 10, attributes: { attachment: attachment } } } + + it 'attachment exceeds maximum limit' do + allow(attachment).to receive(:size) { 100 } + validator.validate_each(note, :attachment, attachment) + expect(note.errors).to have_key(:attachment) + end + + it 'attachment under maximum limit' do + allow(attachment).to receive(:size) { 1 } + validator.validate_each(note, :attachment, attachment) + expect(note.errors).not_to have_key(:attachment) + end + end + + describe 'options uses a symbol' do + let(:options) { { maximum: :test, + attributes: { attachment: attachment } } } + before do + allow(note).to receive(:test) { 10 } + end + + it 'attachment exceeds maximum limit' do + allow(attachment).to receive(:size) { 100 } + validator.validate_each(note, :attachment, attachment) + expect(note.errors).to have_key(:attachment) + end + + it 'attachment under maximum limit' do + allow(attachment).to receive(:size) { 1 } + validator.validate_each(note, :attachment, attachment) + expect(note.errors).not_to have_key(:attachment) + end + end +end diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/git_ref_validator_spec.rb index b2469c18395..4633b6f3934 100644 --- a/spec/lib/git_ref_validator_spec.rb +++ b/spec/lib/git_ref_validator_spec.rb @@ -1,20 +1,20 @@ require 'spec_helper' describe Gitlab::GitRefValidator do - it { Gitlab::GitRefValidator.validate('feature/new').should be_true } - it { Gitlab::GitRefValidator.validate('implement_@all').should be_true } - it { Gitlab::GitRefValidator.validate('my_new_feature').should be_true } - it { Gitlab::GitRefValidator.validate('#1').should be_true } - it { Gitlab::GitRefValidator.validate('feature/~new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/^new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/:new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/?new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/*new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/[new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/new.').should be_false } - it { Gitlab::GitRefValidator.validate('feature\@{').should be_false } - it { Gitlab::GitRefValidator.validate('feature\new').should be_false } - it { Gitlab::GitRefValidator.validate('feature//new').should be_false } - it { Gitlab::GitRefValidator.validate('feature new').should be_false } + it { expect(Gitlab::GitRefValidator.validate('feature/new')).to be_truthy } + it { expect(Gitlab::GitRefValidator.validate('implement_@all')).to be_truthy } + it { expect(Gitlab::GitRefValidator.validate('my_new_feature')).to be_truthy } + it { expect(Gitlab::GitRefValidator.validate('#1')).to be_truthy } + it { expect(Gitlab::GitRefValidator.validate('feature/~new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/^new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/:new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/?new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/*new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/[new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/new.')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature\@{')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature\new')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature//new')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature new')).to be_falsey } end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb new file mode 100644 index 00000000000..23f83339ec5 --- /dev/null +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' +require 'nokogiri' + +module Gitlab + describe Asciidoc do + + let(:input) { '<b>ascii</b>' } + let(:context) { {} } + let(:html) { 'H<sub>2</sub>O' } + + context "without project" do + + it "should convert the input using Asciidoctor and default options" do + expected_asciidoc_opts = { safe: :secure, backend: :html5, + attributes: described_class::DEFAULT_ADOC_ATTRS } + + expect(Asciidoctor).to receive(:convert) + .with(input, expected_asciidoc_opts).and_return(html) + + expect( render(input, context) ).to eql html + end + + context "with asciidoc_opts" do + + let(:asciidoc_opts) { {safe: :safe, attributes: ['foo']} } + + it "should merge the options with default ones" do + expected_asciidoc_opts = { safe: :safe, backend: :html5, + attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo'] } + + expect(Asciidoctor).to receive(:convert) + .with(input, expected_asciidoc_opts).and_return(html) + + render(input, context, asciidoc_opts) + end + end + end + + context "with project in context" do + + let(:context) { {project: create(:project)} } + + it "should filter converted input via HTML pipeline and return result" do + filtered_html = '<b>ASCII</b>' + + allow(Asciidoctor).to receive(:convert).and_return(html) + expect_any_instance_of(HTML::Pipeline).to receive(:call) + .with(html, context) + .and_return(output: Nokogiri::HTML.fragment(filtered_html)) + + expect( render('foo', context) ).to eql filtered_html + end + end + + def render(*args) + described_class.render(*args) + end + end +end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 95fc7e16a11..72806bebe1f 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -27,16 +27,18 @@ describe Gitlab::Auth do it "should not find user with invalid password" do password = 'wrong' - expect( gl_auth.find(username, password) ).to_not eql user + expect( gl_auth.find(username, password) ).not_to eql user end it "should not find user with invalid login" do user = 'wrong' - expect( gl_auth.find(username, password) ).to_not eql user + expect( gl_auth.find(username, password) ).not_to eql user end context "with ldap enabled" do - before { Gitlab::LDAP::Config.stub(enabled?: true) } + before do + allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) + end it "tries to autheticate with db before ldap" do expect(Gitlab::LDAP::Authentication).not_to receive(:login) diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb new file mode 100644 index 00000000000..42c9946d2a9 --- /dev/null +++ b/spec/lib/gitlab/backend/grack_auth_spec.rb @@ -0,0 +1,196 @@ +require "spec_helper" + +describe Grack::Auth do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:app) { lambda { |env| [200, {}, "Success!"] } } + let!(:auth) { Grack::Auth.new(app) } + let(:env) { + { + "rack.input" => "", + "REQUEST_METHOD" => "GET", + "QUERY_STRING" => "service=git-upload-pack" + } + } + 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 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 + for n in 0..maxretry 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 + + for n in 0..maxretry 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" } + + before do + gitlab_ci_service = project.build_gitlab_ci_service + gitlab_ci_service.active = true + gitlab_ci_service.token = token + gitlab_ci_service.project_url = "http://google.com" + gitlab_ci_service.save + + 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/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb index f00ec0fa401..b6d04330599 100644 --- a/spec/lib/gitlab/backend/shell_spec.rb +++ b/spec/lib/gitlab/backend/shell_spec.rb @@ -5,14 +5,14 @@ describe Gitlab::Shell do let(:gitlab_shell) { Gitlab::Shell.new } before do - Project.stub(find: project) + allow(Project).to receive(:find).and_return(project) end - it { should respond_to :add_key } - it { should respond_to :remove_key } - it { should respond_to :add_repository } - it { should respond_to :remove_repository } - it { should respond_to :fork_repository } + it { is_expected.to respond_to :add_key } + it { is_expected.to respond_to :remove_key } + it { is_expected.to respond_to :add_repository } + it { is_expected.to respond_to :remove_repository } + it { is_expected.to respond_to :fork_repository } - it { gitlab_shell.url_to_repo('diaspora').should == Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git" } + it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") } end diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb new file mode 100644 index 00000000000..dd450e9967b --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::BitbucketImport::Client do + 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") + end + + it 'all OAuth client options are symbols' do + client.consumer.options.keys.each do |key| + expect(key).to be_kind_of(Symbol) + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb new file mode 100644 index 00000000000..0ec6a43f681 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Gitlab::BitbucketImport::ProjectCreator do + let(:user) { create(:user, bitbucket_access_token: "asdffg", bitbucket_access_token_secret: "sekret") } + let(:repo) { { + name: 'Vim', + slug: 'vim', + is_private: true, + owner: "asd"}.with_indifferent_access + } + let(:namespace){ create(:group, owner: user) } + + before do + namespace.add_owner(user) + end + + it 'creates project' do + allow_any_instance_of(Project).to receive(:add_import_job) + + project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user) + project = project_creator.execute + + expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git") + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end +end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb new file mode 100644 index 00000000000..5d7ff4f6122 --- /dev/null +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -0,0 +1,176 @@ +require 'spec_helper' + +describe Gitlab::ClosingIssueExtractor do + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + let(:reference) { issue.to_reference } + + subject { described_class.new(project, project.creator) } + + describe "#closed_by_message" do + context 'with a single reference' do + it do + message = "Awesome commit (Closes #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Awesome commit (closes #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Closed #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "closed #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Closing #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "closing #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Close #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "close #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Awesome commit (Fixes #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Awesome commit (fixes #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Fixed #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "fixed #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Fixing #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "fixing #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Fix #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "fix #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Awesome commit (Resolves #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Awesome commit (resolves #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Resolved #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "resolved #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Resolving #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "resolving #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Resolve #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "resolve #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + end + + context 'with multiple references' do + let(:other_issue) { create(:issue, project: project) } + let(:third_issue) { create(:issue, project: project) } + let(:reference2) { other_issue.to_reference } + let(:reference3) { third_issue.to_reference } + + it 'fetches issues in single line message' do + message = "Closes #{reference} and fix #{reference2}" + + expect(subject.closed_by_message(message)). + to eq([issue, other_issue]) + end + + it 'fetches comma-separated issues references in single line message' do + message = "Closes #{reference}, closes #{reference2}" + + expect(subject.closed_by_message(message)). + to eq([issue, other_issue]) + end + + it 'fetches comma-separated issues numbers in single line message' do + message = "Closes #{reference}, #{reference2} and #{reference3}" + + expect(subject.closed_by_message(message)). + to eq([issue, other_issue, third_issue]) + end + + it 'fetches issues in multi-line message' do + message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}" + + expect(subject.closed_by_message(message)). + to eq([issue, other_issue]) + end + + it 'fetches issues in hybrid message' do + message = "Awesome commit (closes #{reference})\n"\ + "Also fixing issues #{reference2}, #{reference3} and #4" + + expect(subject.closed_by_message(message)). + to eq([issue, other_issue, third_issue]) + end + end + end +end diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index cf0b5c282c1..8b7946f3117 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -4,18 +4,18 @@ describe Gitlab::Diff::File do include RepoHelpers let(:project) { create(:project) } - let(:commit) { project.repository.commit(sample_commit.id) } + let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.diffs.first } let(:diff_file) { Gitlab::Diff::File.new(diff) } describe :diff_lines do let(:diff_lines) { diff_file.diff_lines } - it { diff_lines.size.should == 30 } - it { diff_lines.first.should be_kind_of(Gitlab::Diff::Line) } + it { expect(diff_lines.size).to eq(30) } + it { expect(diff_lines.first).to be_kind_of(Gitlab::Diff::Line) } end describe :mode_changed? do - it { diff_file.mode_changed?.should be_false } + it { expect(diff_file.mode_changed?).to be_falsey } end end diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index 35b78260acd..4d5d1431683 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::Diff::Parser do include RepoHelpers let(:project) { create(:project) } - let(:commit) { project.repository.commit(sample_commit.id) } + let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.diffs.first } let(:parser) { Gitlab::Diff::Parser.new } @@ -50,43 +50,43 @@ eos @lines = parser.parse(diff.lines) end - it { @lines.size.should == 30 } + it { expect(@lines.size).to eq(30) } describe 'lines' do describe 'first line' do let(:line) { @lines.first } - it { line.type.should == 'match' } - it { line.old_pos.should == 6 } - it { line.new_pos.should == 6 } - it { line.text.should == '@@ -6,12 +6,18 @@ module Popen' } + it { expect(line.type).to eq('match') } + it { expect(line.old_pos).to eq(6) } + it { expect(line.new_pos).to eq(6) } + it { expect(line.text).to eq('@@ -6,12 +6,18 @@ module Popen') } end describe 'removal line' do let(:line) { @lines[10] } - it { line.type.should == 'old' } - it { line.old_pos.should == 14 } - it { line.new_pos.should == 13 } - it { line.text.should == '- options = { chdir: path }' } + it { expect(line.type).to eq('old') } + it { expect(line.old_pos).to eq(14) } + it { expect(line.new_pos).to eq(13) } + it { expect(line.text).to eq('- options = { chdir: path }') } end describe 'addition line' do let(:line) { @lines[16] } - it { line.type.should == 'new' } - it { line.old_pos.should == 15 } - it { line.new_pos.should == 18 } - it { line.text.should == '+ options = {' } + it { expect(line.type).to eq('new') } + it { expect(line.old_pos).to eq(15) } + it { expect(line.new_pos).to eq(18) } + it { expect(line.text).to eq('+ options = {') } end describe 'unchanged line' do let(:line) { @lines.last } - it { line.type.should == nil } - it { line.old_pos.should == 24 } - it { line.new_pos.should == 31 } - it { line.text.should == ' @cmd_output << stderr.read' } + it { expect(line.type).to eq(nil) } + it { expect(line.old_pos).to eq(24) } + it { expect(line.new_pos).to eq(31) } + it { expect(line.text).to eq(' @cmd_output << stderr.read') } end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 66e87e57cbc..c7291689e32 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -1,18 +1,81 @@ require 'spec_helper' describe Gitlab::GitAccess do - let(:access) { Gitlab::GitAccess.new } + let(:access) { Gitlab::GitAccess.new(actor, project) } let(:project) { create(:project) } let(:user) { create(:user) } + let(:actor) { user } + + describe 'can_push_to_branch?' do + describe 'push to none protected branch' do + it "returns true if user is a master" do + project.team << [user, :master] + expect(access.can_push_to_branch?("random_branch")).to be_truthy + end + + it "returns true if user is a developer" do + project.team << [user, :developer] + expect(access.can_push_to_branch?("random_branch")).to be_truthy + end + + it "returns false if user is a reporter" do + project.team << [user, :reporter] + expect(access.can_push_to_branch?("random_branch")).to be_falsey + end + end + + describe 'push to protected branch' do + before do + @branch = create :protected_branch, project: project + end + + it "returns true if user is a master" do + project.team << [user, :master] + expect(access.can_push_to_branch?(@branch.name)).to be_truthy + end + + it "returns false if user is a developer" do + project.team << [user, :developer] + expect(access.can_push_to_branch?(@branch.name)).to be_falsey + end + + it "returns false if user is a reporter" do + project.team << [user, :reporter] + expect(access.can_push_to_branch?(@branch.name)).to be_falsey + end + end + + describe 'push to protected branch if allowed for developers' do + before do + @branch = create :protected_branch, project: project, developers_can_push: true + end + + it "returns true if user is a master" do + project.team << [user, :master] + expect(access.can_push_to_branch?(@branch.name)).to be_truthy + end + + it "returns true if user is a developer" do + project.team << [user, :developer] + expect(access.can_push_to_branch?(@branch.name)).to be_truthy + end + + it "returns false if user is a reporter" do + project.team << [user, :reporter] + expect(access.can_push_to_branch?(@branch.name)).to be_falsey + end + end + + end describe 'download_access_check' do describe 'master permissions' do before { project.team << [user, :master] } context 'pull code' do - subject { access.download_access_check(user, project) } + subject { access.download_access_check } - it { subject.allowed?.should be_true } + it { expect(subject.allowed?).to be_truthy } end end @@ -20,9 +83,9 @@ describe Gitlab::GitAccess do before { project.team << [user, :guest] } context 'pull code' do - subject { access.download_access_check(user, project) } + subject { access.download_access_check } - it { subject.allowed?.should be_false } + it { expect(subject.allowed?).to be_falsey } end end @@ -33,36 +96,29 @@ describe Gitlab::GitAccess do end context 'pull code' do - subject { access.download_access_check(user, project) } + subject { access.download_access_check } - it { subject.allowed?.should be_false } + it { expect(subject.allowed?).to be_falsey } end end describe 'without acccess to project' do context 'pull code' do - subject { access.download_access_check(user, project) } + subject { access.download_access_check } - it { subject.allowed?.should be_false } + it { expect(subject.allowed?).to be_falsey } end end describe 'deploy key permissions' do let(:key) { create(:deploy_key) } + let(:actor) { key } context 'pull code' do - context 'allowed' do - before { key.projects << project } - subject { access.download_access_check(key, project) } - - it { subject.allowed?.should be_true } - end - - context 'denied' do - subject { access.download_access_check(key, project) } + before { key.projects << project } + subject { access.download_access_check } - it { subject.allowed?.should be_false } - end + it { expect(subject.allowed?).to be_truthy } end end end @@ -129,6 +185,13 @@ describe Gitlab::GitAccess do } end + def self.updated_permissions_matrix + updated_permissions_matrix = permissions_matrix.dup + updated_permissions_matrix[:developer][:push_protected_branch] = true + updated_permissions_matrix[:developer][:push_all] = true + updated_permissions_matrix + end + permissions_matrix.keys.each do |role| describe "#{role} access" do before { protect_feature_branch } @@ -136,9 +199,26 @@ describe Gitlab::GitAccess do permissions_matrix[role].each do |action, allowed| context action do - subject { access.push_access_check(user, project, changes[action]) } + subject { access.push_access_check(changes[action]) } + + it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey } + end + end + end + end + + context "with enabled developers push to protected branches " do + updated_permissions_matrix.keys.each do |role| + describe "#{role} access" do + before { create(:protected_branch, name: 'feature', developers_can_push: true, project: project) } + before { project.team << [user, role] } + + updated_permissions_matrix[role].each do |action, allowed| + context action do + subject { access.push_access_check(changes[action]) } - it { subject.allowed?.should allowed ? be_true : be_false } + it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey } + end end end end diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 4ff45c0c616..4cb91094cb3 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::GitAccessWiki do - let(:access) { Gitlab::GitAccessWiki.new } + let(:access) { Gitlab::GitAccessWiki.new(user, project) } let(:project) { create(:project) } let(:user) { create(:user) } @@ -11,9 +11,9 @@ describe Gitlab::GitAccessWiki do project.team << [user, :developer] end - subject { access.push_access_check(user, project, changes) } + subject { access.push_access_check(changes) } - it { subject.allowed?.should be_true } + it { expect(subject.allowed?).to be_truthy } end def changes diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb new file mode 100644 index 00000000000..26618120316 --- /dev/null +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::Client do + let(:token) { '123456' } + let(:client) { Gitlab::GithubImport::Client.new(token) } + + before do + Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "github") + end + + it 'all OAuth2 client options are symbols' do + client.client.options.keys.each do |key| + expect(key).to be_kind_of(Symbol) + end + end +end diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb new file mode 100644 index 00000000000..3bf52cb685e --- /dev/null +++ b/spec/lib/gitlab/github_import/project_creator_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::ProjectCreator do + let(:user) { create(:user, github_access_token: "asdffg") } + let(:repo) { OpenStruct.new( + login: 'vim', + name: 'vim', + private: true, + full_name: 'asd/vim', + clone_url: "https://gitlab.com/asd/vim.git", + owner: OpenStruct.new(login: "john")) + } + let(:namespace){ create(:group, owner: user) } + + before do + namespace.add_owner(user) + end + + it 'creates project' do + allow_any_instance_of(Project).to receive(:add_import_job) + + project_creator = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, user) + project = project_creator.execute + + expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git") + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end +end diff --git a/spec/lib/gitlab/gitlab_import/client_spec.rb b/spec/lib/gitlab/gitlab_import/client_spec.rb new file mode 100644 index 00000000000..c511c515474 --- /dev/null +++ b/spec/lib/gitlab/gitlab_import/client_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Gitlab::GitlabImport::Client do + 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") + end + + it 'all OAuth2 client options are symbols' do + client.client.options.keys.each do |key| + expect(key).to be_kind_of(Symbol) + end + end +end diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb new file mode 100644 index 00000000000..3cefe4ea8e2 --- /dev/null +++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::GitlabImport::ProjectCreator do + let(:user) { create(:user, gitlab_access_token: "asdffg") } + let(:repo) { { + name: 'vim', + path: 'vim', + visibility_level: Gitlab::VisibilityLevel::PRIVATE, + path_with_namespace: 'asd/vim', + http_url_to_repo: "https://gitlab.com/asd/vim.git", + owner: {name: "john"}}.with_indifferent_access + } + let(:namespace){ create(:group, owner: user) } + + before do + namespace.add_owner(user) + end + + it 'creates project' do + allow_any_instance_of(Project).to receive(:add_import_job) + + project_creator = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, user) + project = project_creator.execute + + expect(project.import_url).to eq("https://oauth2:asdffg@gitlab.com/asd/vim.git") + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end +end diff --git a/spec/lib/gitlab/gitlab_markdown_helper_spec.rb b/spec/lib/gitlab/gitlab_markdown_helper_spec.rb deleted file mode 100644 index 540618a5603..00000000000 --- a/spec/lib/gitlab/gitlab_markdown_helper_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe Gitlab::MarkdownHelper do - describe '#markup?' do - %w(textile rdoc org creole wiki - mediawiki rst adoc asciidoc asc).each do |type| - it "returns true for #{type} files" do - Gitlab::MarkdownHelper.markup?("README.#{type}").should be_true - end - end - - it 'returns false when given a non-markup filename' do - Gitlab::MarkdownHelper.markup?('README.rb').should_not be_true - end - end - - describe '#gitlab_markdown?' do - %w(mdown md markdown).each do |type| - it "returns true for #{type} files" do - Gitlab::MarkdownHelper.gitlab_markdown?("README.#{type}").should be_true - end - end - - it 'returns false when given a non-markdown filename' do - Gitlab::MarkdownHelper.gitlab_markdown?('README.rb').should_not be_true - end - end -end diff --git a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb new file mode 100644 index 00000000000..c1125ca6357 --- /dev/null +++ b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Gitlab::GitoriousImport::ProjectCreator do + let(:user) { create(:user) } + let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') } + let(:namespace){ create(:group, owner: user) } + + before do + namespace.add_owner(user) + end + + it 'creates project' do + allow_any_instance_of(Project).to receive(:add_import_job) + + project_creator = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, user) + project = project_creator.execute + + expect(project.name).to eq("Bar Baz Qux") + expect(project.path).to eq("bar-baz-qux") + expect(project.namespace).to eq(namespace) + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + expect(project.import_type).to eq("gitorious") + expect(project.import_source).to eq("foo/bar-baz-qux") + expect(project.import_url).to eq("https://gitorious.org/foo/bar-baz-qux.git") + end +end diff --git a/spec/lib/gitlab/google_code_import/client_spec.rb b/spec/lib/gitlab/google_code_import/client_spec.rb new file mode 100644 index 00000000000..6aa4428f367 --- /dev/null +++ b/spec/lib/gitlab/google_code_import/client_spec.rb @@ -0,0 +1,35 @@ +require "spec_helper" + +describe Gitlab::GoogleCodeImport::Client do + let(:raw_data) { JSON.parse(File.read(Rails.root.join("spec/fixtures/GoogleCodeProjectHosting.json"))) } + subject { described_class.new(raw_data) } + + describe "#valid?" do + context "when the data is valid" do + it "returns true" do + expect(subject).to be_valid + end + end + + context "when the data is invalid" do + let(:raw_data) { "No clue" } + + it "returns true" do + expect(subject).not_to be_valid + end + end + end + + describe "#repos" do + it "returns only Git repositories" do + expect(subject.repos.length).to eq(1) + expect(subject.incompatible_repos.length).to eq(1) + end + end + + describe "#repo" do + it "returns the referenced repository" do + expect(subject.repo("tint2").name).to eq("tint2") + end + end +end diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb new file mode 100644 index 00000000000..6a7a31239c3 --- /dev/null +++ b/spec/lib/gitlab/google_code_import/importer_spec.rb @@ -0,0 +1,85 @@ +require "spec_helper" + +describe Gitlab::GoogleCodeImport::Importer do + let(:mapped_user) { create(:user, username: "thilo123") } + let(:raw_data) { JSON.parse(File.read(Rails.root.join("spec/fixtures/GoogleCodeProjectHosting.json"))) } + let(:client) { Gitlab::GoogleCodeImport::Client.new(raw_data) } + let(:import_data) { + { + "repo" => client.repo("tint2").raw_data, + "user_map" => { + "thilo..." => "@#{mapped_user.username}" + } + } + } + let(:project) { create(:project) } + subject { described_class.new(project) } + + before do + project.create_import_data(data: import_data) + end + + describe "#execute" do + + it "imports status labels" do + subject.execute + + %w(New NeedInfo Accepted Wishlist Started Fixed Invalid Duplicate WontFix Incomplete).each do |status| + expect(project.labels.find_by(name: "Status: #{status}")).not_to be_nil + end + end + + it "imports labels" do + subject.execute + + %w( + Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical + Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security + Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery + Component-Systray Component-Clock Component-Launcher Component-Tint2conf Component-Docs Component-New + ).each do |label| + label.sub!("-", ": ") + expect(project.labels.find_by(name: label)).not_to be_nil + end + end + + it "imports issues" do + subject.execute + + issue = project.issues.first + expect(issue).not_to be_nil + expect(issue.iid).to eq(169) + expect(issue.author).to eq(project.creator) + expect(issue.assignee).to eq(mapped_user) + expect(issue.state).to eq("closed") + expect(issue.label_names).to include("Priority: Medium") + expect(issue.label_names).to include("Status: Fixed") + expect(issue.label_names).to include("Type: Enhancement") + expect(issue.title).to eq("Scrolling through tasks") + expect(issue.state).to eq("closed") + expect(issue.description).to include("schattenpr\\.\\.\\.") + expect(issue.description).to include("November 18, 2009 00:20") + expect(issue.description).to include("Google Code") + expect(issue.description).to include('I like to scroll through the tasks with my scrollwheel (like in fluxbox).') + expect(issue.description).to include('Patch is attached that adds two new mouse-actions (next_task+prev_task)') + expect(issue.description).to include('that can be used for exactly that purpose.') + expect(issue.description).to include('all the best!') + expect(issue.description).to include('[tint2_task_scrolling.diff](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/tint2_task_scrolling.diff)') + expect(issue.description).to include('') + end + + it "imports issue comments" do + subject.execute + + note = project.issues.first.notes.first + expect(note).not_to be_nil + expect(note.note).to include("Comment 1") + expect(note.note).to include("@#{mapped_user.username}") + expect(note.note).to include("November 18, 2009 05:14") + expect(note.note).to include("applied, thanks.") + expect(note.note).to include("Status: Fixed") + expect(note.note).to include("~~Type: Defect~~") + expect(note.note).to include("Type: Enhancement") + end + end +end diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb new file mode 100644 index 00000000000..7a224538b8b --- /dev/null +++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::GoogleCodeImport::ProjectCreator do + let(:user) { create(:user) } + let(:repo) { + Gitlab::GoogleCodeImport::Repository.new( + "name" => 'vim', + "summary" => 'VI Improved', + "repositoryUrls" => [ "https://vim.googlecode.com/git/" ] + ) + } + let(:namespace){ create(:group, owner: user) } + + before do + namespace.add_owner(user) + end + + it 'creates project' do + allow_any_instance_of(Project).to receive(:add_import_job) + + project_creator = Gitlab::GoogleCodeImport::ProjectCreator.new(repo, namespace, user) + project = project_creator.execute + + expect(project.import_url).to eq("https://vim.googlecode.com/git/") + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end +end diff --git a/spec/lib/gitlab/key_fingerprint_spec.rb b/spec/lib/gitlab/key_fingerprint_spec.rb new file mode 100644 index 00000000000..266eab6e793 --- /dev/null +++ b/spec/lib/gitlab/key_fingerprint_spec.rb @@ -0,0 +1,12 @@ +require "spec_helper" + +describe Gitlab::KeyFingerprint do + let(:key) { "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" } + let(:fingerprint) { "3f:a2:ee:de:b5:de:53:c3:aa:2f:9c:45:24:4c:47:7b" } + + describe "#fingerprint" do + it "generates the key's fingerprint" do + expect(Gitlab::KeyFingerprint.new(key).fingerprint).to eq(fingerprint) + end + end +end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index f4d5a927396..c38f212b405 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -2,40 +2,85 @@ require 'spec_helper' describe Gitlab::LDAP::Access do let(:access) { Gitlab::LDAP::Access.new user } - let(:user) { create(:user, :ldap) } + let(:user) { create(:omniauth_user) } describe :allowed? do subject { access.allowed? } context 'when the user cannot be found' do - before { Gitlab::LDAP::Person.stub(find_by_dn: nil) } + before do + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(nil) + end - it { should be_false } + it { is_expected.to be_falsey } end context 'when the user is found' do - before { Gitlab::LDAP::Person.stub(find_by_dn: :ldap_user) } + before do + allow(Gitlab::LDAP::Person). + to receive(:find_by_dn).and_return(:ldap_user) + end + + context 'and the user is disabled via active directory' do + before do + allow(Gitlab::LDAP::Person). + to receive(:disabled_via_active_directory?).and_return(true) + end - context 'and the user is diabled via active directory' do - before { Gitlab::LDAP::Person.stub(disabled_via_active_directory?: true) } + it { is_expected.to be_falsey } - it { should be_false } + it "should block user in GitLab" do + access.allowed? + expect(user).to be_blocked + end end context 'and has no disabled flag in active diretory' do - before { Gitlab::LDAP::Person.stub(disabled_via_active_directory?: false) } + before do + user.block + + allow(Gitlab::LDAP::Person). + to receive(:disabled_via_active_directory?).and_return(false) + end - it { should be_true } + it { is_expected.to be_truthy } + + context 'when auto-created users are blocked' do + + before do + allow_any_instance_of(Gitlab::LDAP::Config). + to receive(:block_auto_created_users).and_return(true) + end + + it "does not unblock user in GitLab" do + access.allowed? + expect(user).to be_blocked + end + end + + context "when auto-created users are not blocked" do + + before do + allow_any_instance_of(Gitlab::LDAP::Config). + to receive(:block_auto_created_users).and_return(false) + end + + it "should unblock user in GitLab" do + access.allowed? + expect(user).not_to be_blocked + end + end end context 'without ActiveDirectory enabled' do before do - Gitlab::LDAP::Config.stub(enabled?: true) - Gitlab::LDAP::Config.any_instance.stub(active_directory: false) + allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) + allow_any_instance_of(Gitlab::LDAP::Config). + to receive(:active_directory).and_return(false) end - it { should be_true } + it { is_expected.to be_truthy } end end end -end
\ No newline at end of file +end diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index 19347e47378..38076602df9 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -3,29 +3,34 @@ require 'spec_helper' describe Gitlab::LDAP::Adapter do let(:adapter) { Gitlab::LDAP::Adapter.new 'ldapmain' } - describe :dn_matches_filter? do + describe '#dn_matches_filter?' do let(:ldap) { double(:ldap) } subject { adapter.dn_matches_filter?(:dn, :filter) } - before { adapter.stub(ldap: ldap) } + before { allow(adapter).to receive(:ldap).and_return(ldap) } context "when the search is successful" do context "and the result is non-empty" do - before { ldap.stub(search: [:foo]) } + before { allow(ldap).to receive(:search).and_return([:foo]) } - it { should be_true } + it { is_expected.to be_truthy } end context "and the result is empty" do - before { ldap.stub(search: []) } + before { allow(ldap).to receive(:search).and_return([]) } - it { should be_false } + it { is_expected.to be_falsey } end end context "when the search encounters an error" do - before { ldap.stub(search: nil, get_operation_result: double(code: 1, message: 'some error')) } + before do + allow(ldap).to receive_messages( + search: nil, + get_operation_result: double(code: 1, message: 'some error') + ) + end - it { should be_false } + it { is_expected.to be_falsey } end end end diff --git a/spec/lib/gitlab/ldap/authentication_spec.rb b/spec/lib/gitlab/ldap/authentication_spec.rb index 0eb7c443b8b..6e3de914a45 100644 --- a/spec/lib/gitlab/ldap/authentication_spec.rb +++ b/spec/lib/gitlab/ldap/authentication_spec.rb @@ -1,53 +1,58 @@ require 'spec_helper' describe Gitlab::LDAP::Authentication do - let(:klass) { Gitlab::LDAP::Authentication } - let(:user) { create(:user, :ldap, extern_uid: dn) } - let(:dn) { 'uid=john,ou=people,dc=example,dc=com' } - let(:login) { 'john' } + let(:user) { create(:omniauth_user, extern_uid: dn) } + let(:dn) { 'uid=john,ou=people,dc=example,dc=com' } + let(:login) { 'john' } let(:password) { 'password' } - describe :login do - let(:adapter) { double :adapter } + describe 'login' do before do - Gitlab::LDAP::Config.stub(enabled?: true) + allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) end it "finds the user if authentication is successful" do - user + expect(user).not_to be_nil + # try only to fake the LDAP call - klass.any_instance.stub(adapter: double(:adapter, - bind_as: double(:ldap_user, dn: dn) - )) - expect(klass.login(login, password)).to be_true + adapter = double('adapter', dn: dn).as_null_object + allow_any_instance_of(described_class). + to receive(:adapter).and_return(adapter) + + expect(described_class.login(login, password)).to be_truthy end it "is false if the user does not exist" do # try only to fake the LDAP call - klass.any_instance.stub(adapter: double(:adapter, - bind_as: double(:ldap_user, dn: dn) - )) - expect(klass.login(login, password)).to be_false + adapter = double('adapter', dn: dn).as_null_object + allow_any_instance_of(described_class). + to receive(:adapter).and_return(adapter) + + expect(described_class.login(login, password)).to be_falsey end it "is false if authentication fails" do - user + expect(user).not_to be_nil + # try only to fake the LDAP call - klass.any_instance.stub(adapter: double(:adapter, bind_as: nil)) - expect(klass.login(login, password)).to be_false + adapter = double('adapter', bind_as: nil).as_null_object + allow_any_instance_of(described_class). + to receive(:adapter).and_return(adapter) + + expect(described_class.login(login, password)).to be_falsey end it "fails if ldap is disabled" do - Gitlab::LDAP::Config.stub(enabled?: false) - expect(klass.login(login, password)).to be_false + allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(false) + expect(described_class.login(login, password)).to be_falsey end it "fails if no login is supplied" do - expect(klass.login('', password)).to be_false + expect(described_class.login('', password)).to be_falsey end it "fails if no password is supplied" do - expect(klass.login(login, '')).to be_false + expect(described_class.login(login, '')).to be_falsey end end -end
\ No newline at end of file +end diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 3ebb8aae243..3548d647c84 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::LDAP::Config do let(:config) { Gitlab::LDAP::Config.new provider } let(:provider) { 'ldapmain' } - describe :initalize do + describe '#initalize' do it 'requires a provider' do expect{ Gitlab::LDAP::Config.new }.to raise_error ArgumentError end @@ -14,21 +14,7 @@ describe Gitlab::LDAP::Config do end it "raises an error if a unknow provider is used" do - expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error - end - - context "if 'ldap' is the provider name" do - let(:provider) { 'ldap' } - - context "and 'ldap' is not in defined as a provider" do - before { Gitlab::LDAP::Config.stub(providers: %w{ldapmain}) } - - it "uses the first provider" do - # Fetch the provider_name attribute from 'options' so that we know - # that the 'options' Hash is not empty/nil. - expect(config.options['provider_name']).to eq('ldapmain') - end - end + expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error(RuntimeError) end end end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 726c9764e3d..7cfca96f4e0 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe Gitlab::LDAP::User do - let(:gl_user) { Gitlab::LDAP::User.new(auth_hash) } + let(:ldap_user) { Gitlab::LDAP::User.new(auth_hash) } + let(:gl_user) { ldap_user.gl_user } let(:info) do { name: 'John', @@ -13,24 +14,97 @@ describe Gitlab::LDAP::User do double(uid: 'my-uid', provider: 'ldapmain', info: double(info)) end + describe :changed? do + it "marks existing ldap user as changed" do + create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') + expect(ldap_user.changed?).to be_truthy + end + + it "marks existing non-ldap user if the email matches as changed" do + create(:user, email: 'john@example.com') + expect(ldap_user.changed?).to be_truthy + end + + it "dont marks existing ldap user as changed" do + create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain') + expect(ldap_user.changed?).to be_falsey + end + end + describe :find_or_create do it "finds the user if already existing" do - existing_user = create(:user, extern_uid: 'my-uid', provider: 'ldapmain') + create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') - expect{ gl_user.save }.to_not change{ User.count } + expect{ ldap_user.save }.not_to change{ User.count } end it "connects to existing non-ldap user if the email matches" do - existing_user = create(:user, email: 'john@example.com') - expect{ gl_user.save }.to_not change{ User.count } + existing_user = create(:omniauth_user, email: 'john@example.com', provider: "twitter") + expect{ ldap_user.save }.not_to change{ User.count } existing_user.reload - expect(existing_user.extern_uid).to eql 'my-uid' - expect(existing_user.provider).to eql 'ldapmain' + expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' + expect(existing_user.ldap_identity.provider).to eql 'ldapmain' end it "creates a new user if not found" do - expect{ gl_user.save }.to change{ User.count }.by(1) + expect{ ldap_user.save }.to change{ User.count }.by(1) + end + end + + describe 'blocking' do + def configure_block(value) + allow_any_instance_of(Gitlab::LDAP::Config). + to receive(:block_auto_created_users).and_return(value) + end + + context 'signup' do + context 'dont block on create' do + before { configure_block(false) } + + it do + ldap_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create' do + before { configure_block(true) } + + it do + ldap_user.save + expect(gl_user).to be_valid + expect(gl_user).to be_blocked + end + end + end + + context 'sign-in' do + before do + ldap_user.save + ldap_user.gl_user.activate + end + + context 'dont block on create' do + before { configure_block(false) } + + it do + ldap_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create' do + before { configure_block(true) } + + it do + ldap_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end end end end diff --git a/spec/lib/gitlab/markdown/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/autolink_filter_spec.rb new file mode 100644 index 00000000000..a14cb2da089 --- /dev/null +++ b/spec/lib/gitlab/markdown/autolink_filter_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe AutolinkFilter do + include FilterSpecHelper + + let(:link) { 'http://about.gitlab.com/' } + + it 'does nothing when :autolink is false' do + exp = act = link + expect(filter(act, autolink: false).to_html).to eq exp + end + + it 'does nothing with non-link text' do + exp = act = 'This text contains no links to autolink' + expect(filter(act).to_html).to eq exp + end + + context 'Rinku schemes' do + it 'autolinks http' do + doc = filter("See #{link}") + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'autolinks https' do + link = 'https://google.com/' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'autolinks ftp' do + link = 'ftp://ftp.us.debian.org/debian/' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'autolinks short URLs' do + link = 'http://localhost:3000/' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'accepts link_attr options' do + doc = filter("See #{link}", link_attr: {class: 'custom'}) + + expect(doc.at_css('a')['class']).to eq 'custom' + end + + described_class::IGNORE_PARENTS.each do |elem| + it "ignores valid links contained inside '#{elem}' element" do + exp = act = "<#{elem}>See #{link}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + end + + context 'other schemes' do + let(:link) { 'foo://bar.baz/' } + + it 'autolinks smb' do + link = 'smb:///Volumes/shared/foo.pdf' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'autolinks irc' do + link = 'irc://irc.freenode.net/git' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'does not include trailing punctuation' do + doc = filter("See #{link}.") + expect(doc.at_css('a').text).to eq link + + doc = filter("See #{link}, ok?") + expect(doc.at_css('a').text).to eq link + end + + it 'accepts link_attr options' do + doc = filter("See #{link}", link_attr: {class: 'custom'}) + expect(doc.at_css('a')['class']).to eq 'custom' + end + + described_class::IGNORE_PARENTS.each do |elem| + it "ignores valid links contained inside '#{elem}' element" do + exp = act = "<#{elem}>See #{link}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb new file mode 100644 index 00000000000..e8391cc7aca --- /dev/null +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -0,0 +1,148 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe CommitRangeReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:project) } + let(:commit1) { project.commit } + let(:commit2) { project.commit("HEAD~2") } + + let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}") } + let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}") } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Commit Range #{range.to_reference}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { range.to_reference } + let(:reference2) { range2.to_reference } + + it 'links to a valid two-dot reference' do + doc = filter("See #{reference2}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param) + end + + it 'links to a valid three-dot reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param) + end + + it 'links to a valid short ID' do + reference = "#{commit1.short_id}...#{commit2.id}" + reference2 = "#{commit1.id}...#{commit2.short_id}" + + exp = commit1.short_id + '...' + commit2.short_id + + expect(filter("See #{reference}").css('a').first.text).to eq exp + expect(filter("See #{reference2}").css('a').first.text).to eq exp + end + + it 'links with adjacent text' do + doc = filter("See (#{reference}.)") + + exp = Regexp.escape(range.to_s) + expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs' do + exp = act = "See #{commit1.id.reverse}...#{commit2.id}" + + expect(project).to receive(:valid_repo?).and_return(true) + expect(project.repository).to receive(:commit).with(commit1.id.reverse) + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("See #{reference}") + expect(doc.css('a').first.attr('title')).to eq range.reference_title + end + + it 'includes default classes' do + doc = filter("See #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' + end + + it 'includes an optional custom class' do + doc = filter("See #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path option' do + doc = filter("See #{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_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true) + end + + it 'adds to the results hash' do + result = pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, namespace: namespace) } + let(:reference) { range.to_reference(project) } + + before do + range.project = project2 + end + + context 'when user can access reference' do + before { allow_cross_reference! } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + + exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}") + expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" + expect(filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty + end + end + + context 'when user cannot access reference' do + before { disallow_cross_reference! } + + it 'ignores valid references' do + exp = act = "See #{reference}" + + expect(filter(act).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb new file mode 100644 index 00000000000..a10d43c9a02 --- /dev/null +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe CommitReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:project) } + let(:commit) { project.commit } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { commit.id } + + # Let's test a variety of commit SHA sizes just to be paranoid + [6, 8, 12, 18, 20, 32, 40].each do |size| + it "links to a valid reference of #{size} characters" do + doc = filter("See #{reference[0...size]}") + + expect(doc.css('a').first.text).to eq commit.short_id + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project.namespace, project, reference) + end + end + + it 'always uses the short ID as the link text' do + doc = filter("See #{commit.id}") + expect(doc.text).to eq "See #{commit.short_id}" + + doc = filter("See #{commit.id[0...6]}") + expect(doc.text).to eq "See #{commit.short_id}" + end + + it 'links with adjacent text' do + doc = filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs' do + invalid = invalidate_reference(reference) + exp = act = "See #{invalid}" + + expect(project).to receive(:valid_repo?).and_return(true) + expect(project.repository).to receive(:commit).with(invalid) + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("See #{reference}") + expect(doc.css('a').first.attr('title')).to eq commit.link_title + end + + it 'escapes the title attribute' do + allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="}) + + doc = filter("See #{reference}") + expect(doc.text).to eq "See #{commit.short_id}" + end + + it 'includes default classes' do + doc = filter("See #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit' + end + + it 'includes an optional custom class' do + doc = filter("See #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("See #{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_commit_url(project.namespace, project, reference, only_path: true) + end + + it 'adds to the results hash' do + result = pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, namespace: namespace) } + let(:commit) { project2.commit } + let(:reference) { commit.to_reference(project) } + + context 'when user can access reference' do + before { allow_cross_reference! } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + + exp = Regexp.escape(project2.to_reference) + expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty + end + end + + context 'when user cannot access reference' do + before { disallow_cross_reference! } + + it 'ignores valid references' do + exp = act = "See #{reference}" + + expect(filter(act).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb new file mode 100644 index 00000000000..4698d6138c2 --- /dev/null +++ b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe CrossProjectReference do + # context in the html-pipeline sense, not in the rspec sense + let(:context) do + { + current_user: double('user'), + project: double('project') + } + end + + include described_class + + describe '#project_from_ref' do + context 'when no project was referenced' do + it 'returns the project from context' do + expect(project_from_ref(nil)).to eq context[:project] + end + end + + context 'when referenced project does not exist' do + it 'returns nil' do + expect(project_from_ref('invalid/reference')).to be_nil + end + end + + context 'when referenced project exists' do + let(:project2) { double('referenced project') } + + before do + expect(Project).to receive(:find_with_namespace). + with('cross/reference').and_return(project2) + end + + context 'and the user has permission to read it' do + it 'returns the referenced project' do + expect(self).to receive(:user_can_reference_project?). + with(project2).and_return(true) + + expect(project_from_ref('cross/reference')).to eq project2 + end + end + + context 'and the user does not have permission to read it' do + it 'returns nil' do + expect(self).to receive(:user_can_reference_project?). + with(project2).and_return(false) + + expect(project_from_ref('cross/reference')).to be_nil + end + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/emoji_filter_spec.rb new file mode 100644 index 00000000000..11efd9bb4cd --- /dev/null +++ b/spec/lib/gitlab/markdown/emoji_filter_spec.rb @@ -0,0 +1,95 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe EmojiFilter do + include FilterSpecHelper + + before do + ActionController::Base.asset_host = 'https://foo.com' + end + + it 'replaces supported emoji' do + doc = filter('<p>:heart:</p>') + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png' + end + + it 'ignores unsupported emoji' do + exp = act = '<p>:foo:</p>' + doc = filter(act) + expect(doc.to_html).to match Regexp.escape(exp) + end + + it 'correctly encodes the URL' do + doc = filter('<p>:+1:</p>') + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png' + end + + it 'matches at the start of a string' do + doc = filter(':+1:') + expect(doc.css('img').size).to eq 1 + end + + it 'matches at the end of a string' do + doc = filter('This gets a :-1:') + expect(doc.css('img').size).to eq 1 + end + + it 'matches with adjacent text' do + doc = filter('+1 (:+1:)') + expect(doc.css('img').size).to eq 1 + end + + it 'matches multiple emoji in a row' do + doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:') + expect(doc.css('img').size).to eq 3 + end + + it 'has a title attribute' do + doc = filter(':-1:') + expect(doc.css('img').first.attr('title')).to eq ':-1:' + end + + it 'has an alt attribute' do + doc = filter(':-1:') + expect(doc.css('img').first.attr('alt')).to eq ':-1:' + end + + it 'has an align attribute' do + doc = filter(':8ball:') + expect(doc.css('img').first.attr('align')).to eq 'absmiddle' + end + + it 'has an emoji class' do + doc = filter(':cat:') + expect(doc.css('img').first.attr('class')).to eq 'emoji' + end + + it 'has height and width attributes' do + doc = filter(':dog:') + img = doc.css('img').first + + expect(img.attr('width')).to eq '20' + expect(img.attr('height')).to eq '20' + end + + it 'keeps whitespace intact' do + doc = filter('This deserves a :+1:, big time.') + + expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/) + end + + it 'uses a custom asset_root context' do + root = Gitlab.config.gitlab.url + 'gitlab/root' + + doc = filter(':smile:', asset_root: root) + expect(doc.css('img').first.attr('src')).to start_with(root) + end + + it 'uses a custom asset_host context' do + ActionController::Base.asset_host = 'https://cdn.example.com' + + doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?') + expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com') + end + end +end diff --git a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb new file mode 100644 index 00000000000..f16095bc2b2 --- /dev/null +++ b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe ExternalIssueReferenceFilter do + include FilterSpecHelper + + def helper + IssuesHelper + end + + let(:project) { create(:jira_project) } + + context 'JIRA issue references' do + let(:issue) { ExternalIssue.new('JIRA-123', project) } + let(:reference) { issue.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Issue #{reference}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + it 'ignores valid references when using default tracker' do + expect(project).to receive(:default_issues_tracker?).and_return(true) + + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('href')) + .to eq helper.url_for_issue(reference, project) + end + + it 'links to the external tracker' do + doc = filter("Issue #{reference}") + link = doc.css('a').first.attr('href') + + expect(link).to eq "http://jira.example/browse/#{reference}" + end + + it 'links with adjacent text' do + doc = filter("Issue (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) + end + + it 'includes a title attribute' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker" + end + + it 'escapes the title attribute' do + allow(project.external_issue_tracker).to receive(:title). + and_return(%{"></a>whatever<a title="}) + + doc = filter("Issue #{reference}") + expect(doc.text).to eq "Issue #{reference}" + end + + it 'includes default classes' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' + end + + it 'includes an optional custom class' do + doc = filter("Issue #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("Issue #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true) + end + end + end +end diff --git a/spec/lib/gitlab/markdown/external_link_filter_spec.rb b/spec/lib/gitlab/markdown/external_link_filter_spec.rb new file mode 100644 index 00000000000..a040b34577b --- /dev/null +++ b/spec/lib/gitlab/markdown/external_link_filter_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe ExternalLinkFilter do + include FilterSpecHelper + + it 'ignores elements without an href attribute' do + exp = act = %q(<a id="ignored">Ignore Me</a>) + expect(filter(act).to_html).to eq exp + end + + it 'ignores non-HTTP(S) links' do + exp = act = %q(<a href="irc://irc.freenode.net/gitlab">IRC</a>) + expect(filter(act).to_html).to eq exp + end + + it 'skips internal links' do + internal = Gitlab.config.gitlab.url + exp = act = %Q(<a href="#{internal}/sign_in">Login</a>) + expect(filter(act).to_html).to eq exp + end + + it 'adds rel="nofollow" to external links' do + act = %q(<a href="https://google.com/">Google</a>) + doc = filter(act) + + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to eq 'nofollow' + end + end +end diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb new file mode 100644 index 00000000000..fa43d33794d --- /dev/null +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe IssueReferenceFilter do + include FilterSpecHelper + + def helper + IssuesHelper + end + + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Issue #{issue.to_reference}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { issue.to_reference } + + it 'ignores valid references when using non-default tracker' do + expect(project).to receive(:get_issue).with(issue.iid).and_return(nil) + + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = filter("Fixed #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid issue IDs' do + invalid = invalidate_reference(reference) + exp = act = "Fixed #{invalid}" + + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}" + end + + it 'escapes the title attribute' do + issue.update_attribute(:title, %{"></a>whatever<a title="}) + + doc = filter("Issue #{reference}") + expect(doc.text).to eq "Issue #{reference}" + end + + it 'includes default classes' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' + end + + it 'includes an optional custom class' do + doc = filter("Issue #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("Issue #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true) + end + + it 'adds to the results hash' do + result = pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { issue.to_reference(project) } + + context 'when user can access reference' do + before { allow_cross_reference! } + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(Project).to receive(:get_issue). + with(issue.iid).and_return(nil) + + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid issue IDs on the referenced project' do + exp = act = "Fixed #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end + + context 'when user cannot access reference' do + before { disallow_cross_reference! } + + it 'ignores valid references' do + exp = act = "See #{reference}" + + expect(filter(act).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb new file mode 100644 index 00000000000..cf3337b1ba1 --- /dev/null +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' +require 'html/pipeline' + +module Gitlab::Markdown + describe LabelReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:empty_project) } + let(:label) { create(:label, project: project) } + let(:reference) { label.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Label #{reference}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + it 'includes default classes' do + doc = filter("Label #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' + end + + it 'includes an optional custom class' do + doc = filter("Label #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("Label #{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_issues_path(project.namespace, project, label_name: label.name) + end + + it 'adds to the results hash' do + result = pipeline_result("Label #{reference}") + expect(result[:references][:label]).to eq [label] + end + + describe 'label span element' do + it 'includes default classes' do + doc = filter("Label #{reference}") + expect(doc.css('a span').first.attr('class')).to eq 'label color-label' + end + + it 'includes a style attribute' do + doc = filter("Label #{reference}") + expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/) + end + end + + context 'Integer-based references' do + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + end + + it 'links with adjacent text' do + doc = filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + end + + it 'ignores invalid label IDs' do + exp = act = "Label #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + end + + context 'String-based single-word references' do + let(:label) { create(:label, name: 'gfm', project: project) } + let(:reference) { "#{Label.reference_prefix}#{label.name}" } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See gfm' + end + + it 'links with adjacent text' do + doc = filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + end + + it 'ignores invalid label names' do + exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}" + + expect(filter(act).to_html).to eq exp + end + end + + context 'String-based multi-word references in quotes' do + let(:label) { create(:label, name: 'gfm references', project: project) } + let(:reference) { label.to_reference(:name) } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See gfm references' + end + + it 'links with adjacent text' do + doc = filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + end + + it 'ignores invalid label names' do + exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}") + + expect(filter(act).to_html).to eq exp + end + end + + describe 'edge cases' do + it 'gracefully handles non-references matching the pattern' do + exp = act = '(format nil "~0f" 3.0) ; 3.0' + expect(filter(act).to_html).to eq exp + end + end + end +end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb new file mode 100644 index 00000000000..5945302a2da --- /dev/null +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -0,0 +1,123 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe MergeRequestReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:project) } + let(:merge) { create(:merge_request, source_project: project) } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Merge #{merge.to_reference}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { merge.to_reference } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_merge_request_url(project.namespace, project, merge) + end + + it 'links with adjacent text' do + doc = filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid merge IDs' do + exp = act = "Merge #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("Merge #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}" + end + + it 'escapes the title attribute' do + merge.update_attribute(:title, %{"></a>whatever<a title="}) + + doc = filter("Merge #{reference}") + expect(doc.text).to eq "Merge #{reference}" + end + + it 'includes default classes' do + doc = filter("Merge #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' + end + + it 'includes an optional custom class' do + doc = filter("Merge #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("Merge #{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_merge_request_url(project.namespace, project, merge, only_path: true) + end + + it 'adds to the results hash' do + result = pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, namespace: namespace) } + let(:merge) { create(:merge_request, source_project: project2) } + let(:reference) { merge.to_reference(project) } + + context 'when user can access reference' do + before { allow_cross_reference! } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_merge_request_url(project2.namespace, + project, merge) + end + + it 'links with adjacent text' do + doc = filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid merge IDs on the referenced project' do + exp = act = "Merge #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] + end + end + + context 'when user cannot access reference' do + before { disallow_cross_reference! } + + it 'ignores valid references' do + exp = act = "See #{reference}" + + expect(filter(act).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb new file mode 100644 index 00000000000..5ee5310825d --- /dev/null +++ b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe RelativeLinkFilter do + def filter(doc) + described_class.call(doc, { + commit: project.commit, + project: project, + project_wiki: project_wiki, + ref: ref, + requested_path: requested_path + }) + end + + def image(path) + %(<img src="#{path}" />) + end + + def link(path) + %(<a href="#{path}">#{path}</a>) + end + + let(:project) { create(:project) } + let(:project_path) { project.path_with_namespace } + let(:ref) { 'markdown' } + let(:project_wiki) { nil } + let(:requested_path) { '/' } + + shared_examples :preserve_unchanged do + it 'does not modify any relative URL in anchor' do + doc = filter(link('README.md')) + expect(doc.at_css('a')['href']).to eq 'README.md' + end + + it 'does not modify any relative URL in image' do + doc = filter(image('files/images/logo-black.png')) + expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' + end + end + + shared_examples :relative_to_requested do + it 'rebuilds URL relative to the requested path' do + doc = filter(link('users.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/users.md" + end + end + + context 'with a project_wiki' do + let(:project_wiki) { double('ProjectWiki') } + include_examples :preserve_unchanged + end + + context 'without a repository' do + let(:project) { create(:empty_project) } + include_examples :preserve_unchanged + end + + context 'with an empty repository' do + let(:project) { create(:project_empty_repo) } + include_examples :preserve_unchanged + end + + it 'does not raise an exception on invalid URIs' do + act = link("://foo") + expect { filter(act) }.not_to raise_error + end + + context 'with a valid repository' do + it 'rebuilds relative URL for a file in the repo' do + doc = filter(link('doc/api/README.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + + it 'rebuilds relative URL for a file in the repo with an anchor' do + doc = filter(link('README.md#section')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/README.md#section" + end + + it 'rebuilds relative URL for a directory in the repo' do + doc = filter(link('doc/api/')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/tree/#{ref}/doc/api" + end + + it 'rebuilds relative URL for an image in the repo' do + doc = filter(link('files/images/logo-black.png')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" + end + + it 'does not modify relative URL with an anchor only' do + doc = filter(link('#section-1')) + expect(doc.at_css('a')['href']).to eq '#section-1' + end + + it 'does not modify absolute URL' do + doc = filter(link('http://example.com')) + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + + context 'when requested path is a file in the repo' do + let(:requested_path) { 'doc/api/README.md' } + include_examples :relative_to_requested + end + + context 'when requested path is a directory in the repo' do + let(:requested_path) { 'doc/api' } + include_examples :relative_to_requested + end + end + end +end diff --git a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb new file mode 100644 index 00000000000..e50c82d0b3c --- /dev/null +++ b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe SanitizationFilter do + include FilterSpecHelper + + describe 'default whitelist' do + it 'sanitizes tags that are not whitelisted' do + act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>} + exp = 'no inputs and no blinks' + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes tag attributes' do + act = %q{<a href="http://example.com/bar.html" onclick="bar">Text</a>} + exp = %q{<a href="http://example.com/bar.html">Text</a>} + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes 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 + end + + it 'sanitizes `class` attribute on any element' do + act = %q{<strong class="foo">Strong</strong>} + expect(filter(act).to_html).to eq %q{<strong>Strong</strong>} + end + + it 'sanitizes `id` attribute on any element' do + act = %q{<em id="foo">Emphasis</em>} + expect(filter(act).to_html).to eq %q{<em>Emphasis</em>} + end + end + + describe 'custom whitelist' do + it 'customizes the whitelist only once' do + instance = described_class.new('Foo') + 3.times { instance.whitelist } + + expect(instance.whitelist[:transformers].size).to eq 4 + end + + it 'allows syntax highlighting' do + exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>} + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes `class` attribute from non-highlight spans' do + act = %q{<span class="k">def</span>} + expect(filter(act).to_html).to eq %q{<span>def</span>} + end + + it 'allows `style` attribute on table elements' do + html = <<-HTML.strip_heredoc + <table> + <tr><th style="text-align: center">Head</th></tr> + <tr><td style="text-align: right">Body</th></tr> + </table> + HTML + + doc = filter(html) + + expect(doc.at_css('th')['style']).to eq 'text-align: center' + expect(doc.at_css('td')['style']).to eq 'text-align: right' + end + + it 'allows `span` elements' do + exp = act = %q{<span>Hello</span>} + expect(filter(act).to_html).to eq exp + end + + it 'removes `rel` attribute from `a` elements' do + doc = filter(%q{<a href="#" rel="nofollow">Link</a>}) + + expect(doc.css('a').size).to eq 1 + expect(doc.at_css('a')['href']).to eq '#' + expect(doc.at_css('a')['rel']).to be_nil + end + + it 'removes script-like `href` attribute from `a` elements' do + html = %q{<a href="javascript:alert('Hi')">Hi</a>} + doc = filter(html) + + expect(doc.css('a').size).to eq 1 + expect(doc.at_css('a')['href']).to be_nil + end + end + + context 'when pipeline is :description' do + it 'uses a stricter whitelist' do + doc = filter('<h1>Description</h1>', pipeline: :description) + expect(doc.to_html.strip).to eq 'Description' + end + + %w(pre code img ol ul li).each do |elem| + it "removes '#{elem}' elements" do + act = "<#{elem}>Description</#{elem}>" + expect(filter(act, pipeline: :description).to_html.strip). + to eq 'Description' + end + end + + %w(b i strong em a ins del sup sub p).each do |elem| + it "still allows '#{elem}' elements" do + exp = act = "<#{elem}>Description</#{elem}>" + expect(filter(act, pipeline: :description).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb new file mode 100644 index 00000000000..38619a3c07f --- /dev/null +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -0,0 +1,121 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe SnippetReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:empty_project) } + let(:snippet) { create(:project_snippet, project: project) } + let(:reference) { snippet.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Snippet #{reference}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_snippet_url(project.namespace, project, snippet) + end + + it 'links with adjacent text' do + doc = filter("Snippet (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid snippet IDs' do + exp = act = "Snippet #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("Snippet #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}" + end + + it 'escapes the title attribute' do + snippet.update_attribute(:title, %{"></a>whatever<a title="}) + + doc = filter("Snippet #{reference}") + expect(doc.text).to eq "Snippet #{reference}" + end + + it 'includes default classes' do + doc = filter("Snippet #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' + end + + it 'includes an optional custom class' do + doc = filter("Snippet #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("Snippet #{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_snippet_url(project.namespace, project, snippet, only_path: true) + end + + it 'adds to the results hash' do + result = pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, namespace: namespace) } + let(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { snippet.to_reference(project) } + + context 'when user can access reference' do + before { allow_cross_reference! } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'links with adjacent text' do + doc = filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] + end + end + + context 'when user cannot access reference' do + before { disallow_cross_reference! } + + it 'ignores valid references' do + exp = act = "See #{reference}" + + expect(filter(act).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb new file mode 100644 index 00000000000..ddf583a72c1 --- /dev/null +++ b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb @@ -0,0 +1,99 @@ +# encoding: UTF-8 + +require 'spec_helper' + +module Gitlab::Markdown + describe TableOfContentsFilter do + include FilterSpecHelper + + def header(level, text) + "<h#{level}>#{text}</h#{level}>\n" + end + + it 'does nothing when :no_header_anchors is truthy' do + exp = act = header(1, 'Header') + expect(filter(act, no_header_anchors: 1).to_html).to eq exp + end + + it 'does nothing with empty headers' do + exp = act = header(1, nil) + expect(filter(act).to_html).to eq exp + end + + 1.upto(6) do |i| + it "processes h#{i} elements" do + html = header(i, "Header #{i}") + doc = filter(html) + + expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}" + end + end + + describe 'anchor tag' do + it 'has an `anchor` class' do + doc = filter(header(1, 'Header')) + expect(doc.css('h1 a').first.attr('class')).to eq 'anchor' + end + + it 'links to the id' do + doc = filter(header(1, 'Header')) + expect(doc.css('h1 a').first.attr('href')).to eq '#header' + end + + describe 'generated IDs' do + it 'translates spaces to dashes' do + doc = filter(header(1, 'This header has spaces in it')) + expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it' + end + + it 'squeezes multiple spaces and dashes' do + doc = filter(header(1, 'This---header is poorly-formatted')) + expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted' + end + + it 'removes punctuation' do + doc = filter(header(1, "This, header! is, filled. with @ punctuation?")) + expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation' + end + + it 'appends a unique number to duplicates' do + doc = filter(header(1, 'One') + header(2, 'One')) + + expect(doc.css('h1 a').first.attr('id')).to eq 'one' + expect(doc.css('h2 a').first.attr('id')).to eq 'one-1' + end + + it 'supports Unicode' do + doc = filter(header(1, '한글')) + expect(doc.css('h1 a').first.attr('id')).to eq '한글' + expect(doc.css('h1 a').first.attr('href')).to eq '#한글' + end + end + end + + describe 'result' do + def result(html) + HTML::Pipeline.new([described_class]).call(html) + end + + let(:results) { result(header(1, 'Header 1') + header(2, 'Header 2')) } + let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) } + + it 'is contained within a `ul` element' do + expect(doc.children.first.name).to eq 'ul' + expect(doc.children.first.attr('class')).to eq 'section-nav' + end + + it 'contains an `li` element for each header' do + expect(doc.css('li').length).to eq 2 + + links = doc.css('li a') + + expect(links.first.attr('href')).to eq '#header-1' + expect(links.first.text).to eq 'Header 1' + expect(links.last.attr('href')).to eq '#header-2' + expect(links.last.text).to eq 'Header 2' + end + end + end +end diff --git a/spec/lib/gitlab/markdown/task_list_filter_spec.rb b/spec/lib/gitlab/markdown/task_list_filter_spec.rb new file mode 100644 index 00000000000..94f39cc966e --- /dev/null +++ b/spec/lib/gitlab/markdown/task_list_filter_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe TaskListFilter do + include FilterSpecHelper + + it 'does not apply `task-list` class to non-task lists' do + exp = act = %(<ul><li>Item</li></ul>) + expect(filter(act).to_html).to eq exp + end + end +end diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb new file mode 100644 index 00000000000..08e6941028f --- /dev/null +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -0,0 +1,131 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe UserReferenceFilter do + include FilterSpecHelper + + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:reference) { user.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + it 'ignores invalid users' do + exp = act = "Hey #{invalidate_reference(reference)}" + expect(filter(act).to_html).to eq(exp) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Hey #{reference}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + context 'mentioning @all' do + let(:reference) { User.reference_prefix + 'all' } + + before do + project.team << [project.creator, :developer] + end + + it 'supports a special @all mention' do + doc = filter("Hey #{reference}") + 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 + + it 'adds to the results hash' do + result = pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq [project.creator] + end + end + + context 'mentioning a user' do + it 'links to a User' do + doc = filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) + end + + it 'links to a User with a period' do + user = create(:user, name: 'alphA.Beta') + + doc = filter("Hey #{user.to_reference}") + expect(doc.css('a').length).to eq 1 + end + + it 'links to a User with an underscore' do + user = create(:user, name: 'ping_pong_king') + + doc = filter("Hey #{user.to_reference}") + expect(doc.css('a').length).to eq 1 + end + + it 'adds to the results hash' do + result = pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq [user] + end + end + + context 'mentioning a group' do + let(:group) { create(:group) } + let(:user) { create(:user) } + let(:reference) { group.to_reference } + + context 'that the current user can read' do + before do + group.add_user(user, Gitlab::Access::DEVELOPER) + end + + it 'links to the Group' do + doc = filter("Hey #{reference}", current_user: user) + expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + end + + it 'adds to the results hash' do + result = pipeline_result("Hey #{reference}", current_user: user) + expect(result[:references][:user]).to eq group.users + end + end + + context 'that the current user cannot read' do + it 'ignores references to the Group' do + doc = filter("Hey #{reference}", current_user: user) + expect(doc.to_html).to eq "Hey #{reference}" + end + + it 'does not add to the results hash' do + result = pipeline_result("Hey #{reference}", current_user: user) + expect(result[:references][:user]).to eq [] + end + end + end + + it 'links with adjacent text' do + skip "TODO (rspeicher): Re-enable when usernames can't end in periods." + doc = filter("Mention me (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) + end + + it 'includes default classes' do + doc = filter("Hey #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' + end + + it 'includes an optional custom class' do + doc = filter("Hey #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("Hey #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.user_path(user) + end + end +end diff --git a/spec/lib/gitlab/markup_helper_spec.rb b/spec/lib/gitlab/markup_helper_spec.rb new file mode 100644 index 00000000000..7e716e866b1 --- /dev/null +++ b/spec/lib/gitlab/markup_helper_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::MarkupHelper do + describe '#markup?' do + %w(textile rdoc org creole wiki + mediawiki rst adoc ad asciidoc mdown md markdown).each do |type| + it "returns true for #{type} files" do + expect(Gitlab::MarkupHelper.markup?("README.#{type}")).to be_truthy + end + end + + it 'returns false when given a non-markup filename' do + expect(Gitlab::MarkupHelper.markup?('README.rb')).not_to be_truthy + end + end + + describe '#gitlab_markdown?' do + %w(mdown md markdown).each do |type| + it "returns true for #{type} files" do + expect(Gitlab::MarkupHelper.gitlab_markdown?("README.#{type}")).to be_truthy + end + end + + it 'returns false when given a non-markdown filename' do + expect(Gitlab::MarkupHelper.gitlab_markdown?('README.rb')).not_to be_truthy + end + end + + describe '#asciidoc?' do + %w(adoc ad asciidoc ADOC).each do |type| + it "returns true for #{type} files" do + expect(Gitlab::MarkupHelper.asciidoc?("README.#{type}")).to be_truthy + end + end + + it 'returns false when given a non-asciidoc filename' do + expect(Gitlab::MarkupHelper.asciidoc?('README.rb')).not_to be_truthy + end + end +end diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb new file mode 100644 index 00000000000..5826144e66b --- /dev/null +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe 'Gitlab::NoteDataBuilder' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:data) { Gitlab::NoteDataBuilder.build(note, user) } + let(:note_url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors + + 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(note_url) + expect(data[:object_kind]).to eq('note') + expect(data[:user]).to eq(user.hook_attrs) + end + + describe 'When asking for a note on commit' do + let(:note) { create(:note_on_commit) } + + it 'returns the note and commit-specific data' do + expect(data).to have_key(:commit) + end + end + + describe 'When asking for a note on commit diff' do + let(:note) { create(:note_on_commit_diff) } + + it 'returns the note and commit-specific data' do + expect(data).to have_key(:commit) + end + 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) } + + it 'returns the note and issue-specific data' do + data[:issue]["updated_at"] = fixed_time + expect(data).to have_key(:issue) + expect(data[:issue]).to eq(issue.hook_attrs) + end + 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) } + + it 'returns the note and merge request data' do + data[:merge_request]["updated_at"] = fixed_time + expect(data).to have_key(:merge_request) + expect(data[:merge_request]).to eq(merge_request.hook_attrs) + end + 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) } + + it 'returns the note and merge request diff data' do + data[:merge_request]["updated_at"] = fixed_time + expect(data).to have_key(:merge_request) + expect(data[:merge_request]).to eq(merge_request.hook_attrs) + end + 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) } + + it 'returns the note and project snippet data' do + data[:snippet]["updated_at"] = fixed_time + expect(data).to have_key(:snippet) + expect(data[:snippet]).to eq(snippet.hook_attrs) + end + end +end diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb new file mode 100644 index 00000000000..5404b506813 --- /dev/null +++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' + +describe Gitlab::OAuth::AuthHash do + let(:auth_hash) do + Gitlab::OAuth::AuthHash.new( + double({ + provider: provider_ascii, + uid: uid_ascii, + info: double(info_hash) + }) + ) + end + + let(:uid_raw) { + "CN=Onur K\xC3\xBC\xC3\xA7\xC3\xBCk,OU=Test,DC=example,DC=net" + } + let(:email_raw) { "onur.k\xC3\xBC\xC3\xA7\xC3\xBCk@example.net" } + let(:nickname_raw) { "ok\xC3\xBC\xC3\xA7\xC3\xBCk" } + let(:first_name_raw) { 'Onur' } + let(:last_name_raw) { "K\xC3\xBC\xC3\xA7\xC3\xBCk" } + let(:name_raw) { "Onur K\xC3\xBC\xC3\xA7\xC3\xBCk" } + + let(:provider_ascii) { 'ldap'.force_encoding(Encoding::ASCII_8BIT) } + let(:uid_ascii) { uid_raw.force_encoding(Encoding::ASCII_8BIT) } + let(:email_ascii) { email_raw.force_encoding(Encoding::ASCII_8BIT) } + let(:nickname_ascii) { nickname_raw.force_encoding(Encoding::ASCII_8BIT) } + let(:first_name_ascii) { first_name_raw.force_encoding(Encoding::ASCII_8BIT) } + let(:last_name_ascii) { last_name_raw.force_encoding(Encoding::ASCII_8BIT) } + let(:name_ascii) { name_raw.force_encoding(Encoding::ASCII_8BIT) } + + let(:provider_utf8) { provider_ascii.force_encoding(Encoding::UTF_8) } + let(:uid_utf8) { uid_ascii.force_encoding(Encoding::UTF_8) } + let(:email_utf8) { email_ascii.force_encoding(Encoding::UTF_8) } + let(:nickname_utf8) { nickname_ascii.force_encoding(Encoding::UTF_8) } + let(:name_utf8) { name_ascii.force_encoding(Encoding::UTF_8) } + + let(:info_hash) { + { + email: email_ascii, + first_name: first_name_ascii, + last_name: last_name_ascii, + name: name_ascii, + nickname: nickname_ascii, + uid: uid_ascii + } + } + + context 'defaults' do + it { expect(auth_hash.provider).to eql provider_utf8 } + it { expect(auth_hash.uid).to eql uid_utf8 } + it { expect(auth_hash.email).to eql email_utf8 } + it { expect(auth_hash.username).to eql nickname_utf8 } + it { expect(auth_hash.name).to eql name_utf8 } + it { expect(auth_hash.password).not_to be_empty } + end + + context 'email not provided' do + before { info_hash.delete(:email) } + + it 'generates a temp email' do + expect( auth_hash.email).to start_with('temp-email-for-oauth') + end + end + + context 'username not provided' do + before { info_hash.delete(:nickname) } + + it 'takes the first part of the email as username' do + expect(auth_hash.username).to eql 'onur-kucuk' + end + end + + context 'name not provided' do + before { info_hash.delete(:name) } + + it 'concats first and lastname as the name' do + expect(auth_hash.name).to eql name_utf8 + end + end + + context 'auth_hash constructed with ASCII-8BIT encoding' do + it 'forces utf8 encoding on uid' do + expect(auth_hash.uid.encoding).to eql Encoding::UTF_8 + end + + it 'forces utf8 encoding on provider' do + expect(auth_hash.provider.encoding).to eql Encoding::UTF_8 + end + + it 'forces utf8 encoding on name' do + expect(auth_hash.name.encoding).to eql Encoding::UTF_8 + end + + it 'forces utf8 encoding on full_name' do + expect(auth_hash.full_name.encoding).to eql Encoding::UTF_8 + end + + it 'forces utf8 encoding on username' do + expect(auth_hash.username.encoding).to eql Encoding::UTF_8 + end + + it 'forces utf8 encoding on email' do + expect(auth_hash.email.encoding).to eql Encoding::UTF_8 + end + + it 'forces utf8 encoding on password' do + expect(auth_hash.password.encoding).to eql Encoding::UTF_8 + end + end +end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb new file mode 100644 index 00000000000..c6cca98a037 --- /dev/null +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -0,0 +1,272 @@ +require 'spec_helper' + +describe Gitlab::OAuth::User do + let(:oauth_user) { Gitlab::OAuth::User.new(auth_hash) } + let(:gl_user) { oauth_user.gl_user } + let(:uid) { 'my-uid' } + let(:provider) { 'my-provider' } + let(:auth_hash) { double(uid: uid, provider: provider, info: double(info_hash)) } + let(:info_hash) do + { + nickname: '-john+gitlab-ETC%.git@gmail.com', + name: 'John', + email: 'john@mail.com' + } + end + let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } + + describe :persisted? do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + it "finds an existing user based on uid and provider (facebook)" do + # FIXME (rspeicher): It's unlikely that this test is actually doing anything + # `auth` is never used and removing it entirely doesn't break the test, so + # what's it doing? + auth = double(info: double(name: 'John'), uid: 'my-uid', provider: 'my-provider') + expect( oauth_user.persisted? ).to be_truthy + end + + it "returns false if use is not found in database" do + allow(auth_hash).to receive(:uid).and_return('non-existing') + expect( oauth_user.persisted? ).to be_falsey + end + end + + describe :save do + def stub_omniauth_config(messages) + allow(Gitlab.config.omniauth).to receive_messages(messages) + end + + def stub_ldap_config(messages) + allow(Gitlab::LDAP::Config).to receive_messages(messages) + end + + let(:provider) { 'twitter' } + + describe 'signup' do + shared_examples "to verify compliance with allow_single_sign_on" do + context "with allow_single_sign_on enabled" do + before { stub_omniauth_config(allow_single_sign_on: true) } + + it "creates a user from Omniauth" do + oauth_user.save + + expect(gl_user).to be_valid + identity = gl_user.identities.first + expect(identity.extern_uid).to eql uid + expect(identity.provider).to eql 'twitter' + end + end + + context "with allow_single_sign_on disabled (Default)" do + before { stub_omniauth_config(allow_single_sign_on: false) } + it "throws an error" do + expect{ oauth_user.save }.to raise_error StandardError + end + end + end + + context "with auto_link_ldap_user disabled (default)" do + before { stub_omniauth_config(auto_link_ldap_user: false) } + include_examples "to verify compliance with allow_single_sign_on" + end + + context "with auto_link_ldap_user enabled" do + before { stub_omniauth_config(auto_link_ldap_user: true) } + + context "and no LDAP provider defined" do + before { stub_ldap_config(providers: []) } + + include_examples "to verify compliance with allow_single_sign_on" + end + + context "and at least one LDAP provider is defined" do + before { stub_ldap_config(providers: %w(ldapmain)) } + + context "and a corresponding LDAP person" do + before do + allow(ldap_user).to receive(:uid) { uid } + allow(ldap_user).to receive(:username) { uid } + allow(ldap_user).to receive(:email) { ['johndoe@example.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) + end + + context "and no account for the LDAP user" do + + it "creates a user with dual LDAP and omniauth identities" do + oauth_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql uid + expect(gl_user.email).to eql 'johndoe@example.com' + expect(gl_user.identities.length).to eql 2 + identities_as_hash = 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: 'twitter', extern_uid: uid } + ]) + end + end + + context "and LDAP user has an account already" do + let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + it "adds the omniauth identity to the LDAP account" do + oauth_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql 'john' + expect(gl_user.email).to eql 'john@example.com' + expect(gl_user.identities.length).to eql 2 + identities_as_hash = 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: 'twitter', extern_uid: uid } + ]) + end + end + end + + context "and no corresponding LDAP person" do + before { allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) } + + include_examples "to verify compliance with allow_single_sign_on" + end + end + end + + end + + describe 'blocking' do + let(:provider) { 'twitter' } + before { stub_omniauth_config(allow_single_sign_on: true) } + + context 'signup with omniauth only' do + context 'dont block on create' do + before { stub_omniauth_config(block_auto_created_users: false) } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create' do + before { stub_omniauth_config(block_auto_created_users: true) } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).to be_blocked + end + end + end + + context 'signup with linked omniauth and LDAP account' do + before do + stub_omniauth_config(auto_link_ldap_user: true) + allow(ldap_user).to receive(:uid) { uid } + allow(ldap_user).to receive(:username) { uid } + allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] } + allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } + allow(oauth_user).to receive(:ldap_person).and_return(ldap_user) + end + + context "and no account for the LDAP user" do + context 'dont block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).to be_blocked + end + end + end + + context 'and LDAP user has an account already' do + let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + + context 'dont block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + end + end + + + context 'sign-in' do + before do + oauth_user.save + oauth_user.gl_user.activate + end + + context 'dont block on create' do + before { stub_omniauth_config(block_auto_created_users: false) } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create' do + before { stub_omniauth_config(block_auto_created_users: true) } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'dont block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + end + end + end +end diff --git a/spec/lib/gitlab/oauth/auth_hash_spec.rb b/spec/lib/gitlab/oauth/auth_hash_spec.rb deleted file mode 100644 index 5eb77b492b2..00000000000 --- a/spec/lib/gitlab/oauth/auth_hash_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'spec_helper' - -describe Gitlab::OAuth::AuthHash do - let(:auth_hash) do - Gitlab::OAuth::AuthHash.new(double({ - provider: 'twitter', - uid: uid, - info: double(info_hash) - })) - end - let(:uid) { 'my-uid' } - let(:email) { 'my-email@example.com' } - let(:nickname) { 'my-nickname' } - let(:info_hash) { - { - email: email, - nickname: nickname, - name: 'John', - first_name: "John", - last_name: "Who" - } - } - - context "defaults" do - it { expect(auth_hash.provider).to eql 'twitter' } - it { expect(auth_hash.uid).to eql uid } - it { expect(auth_hash.email).to eql email } - it { expect(auth_hash.username).to eql nickname } - it { expect(auth_hash.name).to eql "John" } - it { expect(auth_hash.password).to_not be_empty } - end - - context "email not provided" do - before { info_hash.delete(:email) } - it "generates a temp email" do - expect( auth_hash.email).to start_with('temp-email-for-oauth') - end - end - - context "username not provided" do - before { info_hash.delete(:nickname) } - - it "takes the first part of the email as username" do - expect( auth_hash.username ).to eql "my-email" - end - end - - context "name not provided" do - before { info_hash.delete(:name) } - - it "concats first and lastname as the name" do - expect( auth_hash.name ).to eql "John Who" - end - end -end
\ No newline at end of file diff --git a/spec/lib/gitlab/oauth/user_spec.rb b/spec/lib/gitlab/oauth/user_spec.rb deleted file mode 100644 index 8a83a1b2588..00000000000 --- a/spec/lib/gitlab/oauth/user_spec.rb +++ /dev/null @@ -1,108 +0,0 @@ -require 'spec_helper' - -describe Gitlab::OAuth::User do - let(:oauth_user) { Gitlab::OAuth::User.new(auth_hash) } - let(:gl_user) { oauth_user.gl_user } - let(:uid) { 'my-uid' } - let(:provider) { 'my-provider' } - let(:auth_hash) { double(uid: uid, provider: provider, info: double(info_hash)) } - let(:info_hash) do - { - nickname: 'john', - name: 'John', - email: 'john@mail.com' - } - end - - describe :persisted? do - let!(:existing_user) { create(:user, extern_uid: 'my-uid', provider: 'my-provider') } - - it "finds an existing user based on uid and provider (facebook)" do - auth = double(info: double(name: 'John'), uid: 'my-uid', provider: 'my-provider') - expect( oauth_user.persisted? ).to be_true - end - - it "returns false if use is not found in database" do - auth_hash.stub(uid: 'non-existing') - expect( oauth_user.persisted? ).to be_false - end - end - - describe :save do - let(:provider) { 'twitter' } - - describe 'signup' do - context "with allow_single_sign_on enabled" do - before { Gitlab.config.omniauth.stub allow_single_sign_on: true } - - it "creates a user from Omniauth" do - oauth_user.save - - expect(gl_user).to be_valid - expect(gl_user.extern_uid).to eql uid - expect(gl_user.provider).to eql 'twitter' - end - end - - context "with allow_single_sign_on disabled (Default)" do - it "throws an error" do - expect{ oauth_user.save }.to raise_error StandardError - end - end - end - - describe 'blocking' do - let(:provider) { 'twitter' } - before { Gitlab.config.omniauth.stub allow_single_sign_on: true } - - context 'signup' do - context 'dont block on create' do - before { Gitlab.config.omniauth.stub block_auto_created_users: false } - - it do - oauth_user.save - gl_user.should be_valid - gl_user.should_not be_blocked - end - end - - context 'block on create' do - before { Gitlab.config.omniauth.stub block_auto_created_users: true } - - it do - oauth_user.save - gl_user.should be_valid - gl_user.should be_blocked - end - end - end - - context 'sign-in' do - before do - oauth_user.save - oauth_user.gl_user.activate - end - - context 'dont block on create' do - before { Gitlab.config.omniauth.stub block_auto_created_users: false } - - it do - oauth_user.save - gl_user.should be_valid - gl_user.should_not be_blocked - end - end - - context 'block on create' do - before { Gitlab.config.omniauth.stub block_auto_created_users: true } - - it do - oauth_user.save - gl_user.should be_valid - gl_user.should_not be_blocked - end - end - end - end - end -end diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb index 76d506eb3c0..f80d306cfc6 100644 --- a/spec/lib/gitlab/popen_spec.rb +++ b/spec/lib/gitlab/popen_spec.rb @@ -13,8 +13,8 @@ describe 'Gitlab::Popen', no_db: true do @output, @status = @klass.new.popen(%W(ls), path) end - it { @status.should be_zero } - it { @output.should include('cache') } + it { expect(@status).to be_zero } + it { expect(@output).to include('cache') } end context 'non-zero status' do @@ -22,13 +22,13 @@ describe 'Gitlab::Popen', no_db: true do @output, @status = @klass.new.popen(%W(cat NOTHING), path) end - it { @status.should == 1 } - it { @output.should include('No such file or directory') } + it { expect(@status).to eq(1) } + it { expect(@output).to include('No such file or directory') } end context 'unsafe string command' do it 'raises an error when it gets called with a string argument' do - expect { @klass.new.popen('ls', path) }.to raise_error + expect { @klass.new.popen('ls', path) }.to raise_error(RuntimeError) end end @@ -37,8 +37,8 @@ describe 'Gitlab::Popen', no_db: true do @output, @status = @klass.new.popen(%W(ls)) end - it { @status.should be_zero } - it { @output.should include('spec') } + it { expect(@status).to be_zero } + it { expect(@output).to include('spec') } end end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb new file mode 100644 index 00000000000..32a25f08cac --- /dev/null +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::ProjectSearchResults do + let(:project) { create(:project) } + let(:query) { 'hello world' } + + describe 'initialize with empty ref' do + let(:results) { Gitlab::ProjectSearchResults.new(project.id, query, '') } + + it { expect(results.project).to eq(project) } + it { expect(results.repository_ref).to be_nil } + it { expect(results.query).to eq('hello\\ world') } + end + + describe 'initialize with ref' do + let(:ref) { 'refs/heads/test' } + let(:results) { Gitlab::ProjectSearchResults.new(project.id, query, ref) } + + it { expect(results.project).to eq(project) } + it { expect(results.repository_ref).to eq(ref) } + it { expect(results.query).to eq('hello\\ world') } + end +end diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb new file mode 100644 index 00000000000..1b8ba7b4d43 --- /dev/null +++ b/spec/lib/gitlab/push_data_builder_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'Gitlab::PushDataBuilder' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + + describe :build_sample do + let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) } + + it { expect(data).to be_a(Hash) } + it { expect(data[:before]).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } + it { expect(data[:after]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } + it { expect(data[:ref]).to eq('refs/heads/master') } + it { expect(data[:commits].size).to eq(3) } + it { expect(data[:repository][:git_http_url]).to eq(project.http_url_to_repo) } + it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) } + it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) } + it { expect(data[:total_commits_count]).to eq(3) } + end + + describe :build do + let(:data) do + Gitlab::PushDataBuilder.build(project, + user, + Gitlab::Git::BLANK_SHA, + '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b', + 'refs/tags/v1.1.0') + end + + it { expect(data).to be_a(Hash) } + it { expect(data[:before]).to eq(Gitlab::Git::BLANK_SHA) } + it { expect(data[:checkout_sha]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } + it { expect(data[:after]).to eq('8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b') } + it { expect(data[:ref]).to eq('refs/tags/v1.1.0') } + it { expect(data[:commits]).to be_empty } + it { expect(data[:total_commits_count]).to be_zero } + end +end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 23867df39dd..f921dd9cc09 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -1,105 +1,115 @@ require 'spec_helper' describe Gitlab::ReferenceExtractor do - it 'extracts username references' do - subject.analyze('this contains a @user reference', nil) - subject.users.should == [{ project: nil, id: 'user' }] - end + let(:project) { create(:project) } + subject { Gitlab::ReferenceExtractor.new(project, project.creator) } - it 'extracts issue references' do - subject.analyze('this one talks about issue #1234', nil) - subject.issues.should == [{ project: nil, id: '1234' }] - end + it 'accesses valid user objects' do + @u_foo = create(:user, username: 'foo') + @u_bar = create(:user, username: 'bar') + @u_offteam = create(:user, username: 'offteam') - it 'extracts JIRA issue references' do - Gitlab.config.gitlab.stub(:issues_tracker).and_return('jira') - subject.analyze('this one talks about issue JIRA-1234', nil) - subject.issues.should == [{ project: nil, id: 'JIRA-1234' }] - end + project.team << [@u_foo, :reporter] + project.team << [@u_bar, :guest] - it 'extracts merge request references' do - subject.analyze("and here's !43, a merge request", nil) - subject.merge_requests.should == [{ project: nil, id: '43' }] + subject.analyze('@foo, @baduser, @bar, and @offteam') + expect(subject.users).to eq([@u_foo, @u_bar, @u_offteam]) end - it 'extracts snippet ids' do - subject.analyze('snippets like $12 get extracted as well', nil) - subject.snippets.should == [{ project: nil, id: '12' }] - end + it 'ignores user mentions inside specific elements' do + @u_foo = create(:user, username: 'foo') + @u_bar = create(:user, username: 'bar') + @u_offteam = create(:user, username: 'offteam') - it 'extracts commit shas' do - subject.analyze('commit shas 98cf0ae3 are pulled out as Strings', nil) - subject.commits.should == [{ project: nil, id: '98cf0ae3' }] - end + project.team << [@u_foo, :reporter] + project.team << [@u_bar, :guest] - it 'extracts multiple references and preserves their order' do - subject.analyze('@me and @you both care about this', nil) - subject.users.should == [ - { project: nil, id: 'me' }, - { project: nil, id: 'you' } - ] + subject.analyze(%Q{ + Inline code: `@foo` + + Code block: + + ``` + @bar + ``` + + Quote: + + > @offteam + }) + expect(subject.users).to eq([]) end - it 'leaves the original note unmodified' do - text = 'issue #123 is just the worst, @user' - subject.analyze(text, nil) - text.should == 'issue #123 is just the worst, @user' + it 'accesses valid issue objects' do + @i0 = create(:issue, project: project) + @i1 = create(:issue, project: project) + + subject.analyze("#{@i0.to_reference}, #{@i1.to_reference}, and #{Issue.reference_prefix}999.") + expect(subject.issues).to eq([@i0, @i1]) end - it 'handles all possible kinds of references' do - accessors = Gitlab::Markdown::TYPES.map { |t| "#{t}s".to_sym } - subject.should respond_to(*accessors) + it 'accesses valid merge requests' do + @m0 = create(:merge_request, source_project: project, target_project: project, source_branch: 'aaa') + @m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'bbb') + + subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.") + expect(subject.merge_requests).to eq([@m1, @m0]) end - context 'with a project' do - let(:project) { create(:project) } + it 'accesses valid labels' do + @l0 = create(:label, title: 'one', project: project) + @l1 = create(:label, title: 'two', project: project) + @l2 = create(:label) - it 'accesses valid user objects on the project team' do - @u_foo = create(:user, username: 'foo') - @u_bar = create(:user, username: 'bar') - create(:user, username: 'offteam') + subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}") + expect(subject.labels).to eq([@l0, @l1]) + end - project.team << [@u_foo, :reporter] - project.team << [@u_bar, :guest] + it 'accesses valid snippets' do + @s0 = create(:project_snippet, project: project) + @s1 = create(:project_snippet, project: project) + @s2 = create(:project_snippet) - subject.analyze('@foo, @baduser, @bar, and @offteam', project) - subject.users_for(project).should == [@u_foo, @u_bar] - end + subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}") + expect(subject.snippets).to eq([@s0, @s1]) + end - it 'accesses valid issue objects' do - @i0 = create(:issue, project: project) - @i1 = create(:issue, project: project) + it 'accesses valid commits' do + commit = project.commit('master') - subject.analyze("##{@i0.iid}, ##{@i1.iid}, and #999.", project) - subject.issues_for(project).should == [@i0, @i1] - end + subject.analyze("this references commits #{commit.sha[0..6]} and 012345") + extracted = subject.commits + expect(extracted.size).to eq(1) + expect(extracted[0].sha).to eq(commit.sha) + expect(extracted[0].message).to eq(commit.message) + end - it 'accesses valid merge requests' do - @m0 = create(:merge_request, source_project: project, target_project: project, source_branch: 'aaa') - @m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'bbb') + it 'accesses valid commit ranges' do + commit = project.commit('master') + earlier_commit = project.commit('master~2') - subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.", project) - subject.merge_requests_for(project).should == [@m1, @m0] - end + subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}") - it 'accesses valid snippets' do - @s0 = create(:project_snippet, project: project) - @s1 = create(:project_snippet, project: project) - @s2 = create(:project_snippet) + extracted = subject.commit_ranges + expect(extracted.size).to eq(1) + expect(extracted.first).to be_kind_of(CommitRange) + expect(extracted.first.commit_from).to eq earlier_commit + expect(extracted.first.commit_to).to eq commit + end - subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}", project) - subject.snippets_for(project).should == [@s0, @s1] - end + context 'with a project with an underscore' do + let(:other_project) { create(:project, path: 'test_project') } + let(:issue) { create(:issue, project: other_project) } - it 'accesses valid commits' do - commit = project.repository.commit('master') + before do + other_project.team << [project.creator, :developer] + end - subject.analyze("this references commits #{commit.sha[0..6]} and 012345", - project) - extracted = subject.commits_for(project) - extracted.should have(1).item - extracted[0].sha.should == commit.sha - extracted[0].message.should == commit.message + it 'handles project issue references' do + subject.analyze("this refers issue #{issue.to_reference(project)}") + extracted = subject.issues + expect(extracted.size).to eq(1) + expect(extracted).to eq([issue]) end end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index a3aae7771bd..7fdc8fa600d 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -1,21 +1,24 @@ +# coding: utf-8 require 'spec_helper' describe Gitlab::Regex do - describe 'path regex' do - it { 'gitlab-ce'.should match(Gitlab::Regex.path_regex) } - it { 'gitlab_git'.should match(Gitlab::Regex.path_regex) } - it { '_underscore.js'.should match(Gitlab::Regex.path_regex) } - it { '100px.com'.should match(Gitlab::Regex.path_regex) } - it { '?gitlab'.should_not match(Gitlab::Regex.path_regex) } - it { 'git lab'.should_not match(Gitlab::Regex.path_regex) } - it { 'gitlab.git'.should_not match(Gitlab::Regex.path_regex) } + describe 'project path regex' do + it { expect('gitlab-ce').to match(Gitlab::Regex.project_path_regex) } + it { expect('gitlab_git').to match(Gitlab::Regex.project_path_regex) } + it { expect('_underscore.js').to match(Gitlab::Regex.project_path_regex) } + it { expect('100px.com').to match(Gitlab::Regex.project_path_regex) } + it { expect('?gitlab').not_to match(Gitlab::Regex.project_path_regex) } + it { expect('git lab').not_to match(Gitlab::Regex.project_path_regex) } + it { expect('gitlab.git').not_to match(Gitlab::Regex.project_path_regex) } end describe 'project name regex' do - it { 'gitlab-ce'.should match(Gitlab::Regex.project_name_regex) } - it { 'GitLab CE'.should match(Gitlab::Regex.project_name_regex) } - it { '100 lines'.should match(Gitlab::Regex.project_name_regex) } - it { 'gitlab.git'.should match(Gitlab::Regex.project_name_regex) } - it { '?gitlab'.should_not match(Gitlab::Regex.project_name_regex) } + it { expect('gitlab-ce').to match(Gitlab::Regex.project_name_regex) } + it { expect('GitLab CE').to match(Gitlab::Regex.project_name_regex) } + it { expect('100 lines').to match(Gitlab::Regex.project_name_regex) } + it { expect('gitlab.git').to match(Gitlab::Regex.project_name_regex) } + it { expect('Český název').to match(Gitlab::Regex.project_name_regex) } + it { expect('Dash – is this').to match(Gitlab::Regex.project_name_regex) } + it { expect('?gitlab').not_to match(Gitlab::Regex.project_name_regex) } end end diff --git a/spec/lib/gitlab/satellite/action_spec.rb b/spec/lib/gitlab/satellite/action_spec.rb index 3eb1258d67e..28e3d64ee2b 100644 --- a/spec/lib/gitlab/satellite/action_spec.rb +++ b/spec/lib/gitlab/satellite/action_spec.rb @@ -6,7 +6,7 @@ describe 'Gitlab::Satellite::Action' do describe '#prepare_satellite!' do it 'should be able to fetch timeout from conf' do - Gitlab::Satellite::Action::DEFAULT_OPTIONS[:git_timeout].should == 30.seconds + expect(Gitlab::Satellite::Action::DEFAULT_OPTIONS[:git_timeout]).to eq(30.seconds) end it 'create a repository with a parking branch and one remote: origin' do @@ -15,22 +15,22 @@ describe 'Gitlab::Satellite::Action' do #now lets dirty it up starting_remote_count = repo.git.list_remotes.size - starting_remote_count.should >= 1 + expect(starting_remote_count).to be >= 1 #kind of hookey way to add a second remote origin_uri = repo.git.remote({v: true}).split(" ")[1] begin repo.git.remote({raise: true}, 'add', 'another-remote', origin_uri) repo.git.branch({raise: true}, 'a-new-branch') - repo.heads.size.should > (starting_remote_count) - repo.git.remote().split(" ").size.should > (starting_remote_count) + expect(repo.heads.size).to be > (starting_remote_count) + expect(repo.git.remote().split(" ").size).to be > (starting_remote_count) rescue end repo.git.config({}, "user.name", "#{user.name} -- foo") repo.git.config({}, "user.email", "#{user.email} -- foo") - repo.config['user.name'].should =="#{user.name} -- foo" - repo.config['user.email'].should =="#{user.email} -- foo" + expect(repo.config['user.name']).to eq("#{user.name} -- foo") + expect(repo.config['user.email']).to eq("#{user.email} -- foo") #These must happen in the context of the satellite directory... @@ -42,13 +42,13 @@ describe 'Gitlab::Satellite::Action' do #verify it's clean heads = repo.heads.map(&:name) - heads.size.should == 1 - heads.include?(Gitlab::Satellite::Satellite::PARKING_BRANCH).should == true + expect(heads.size).to eq(1) + expect(heads.include?(Gitlab::Satellite::Satellite::PARKING_BRANCH)).to eq(true) remotes = repo.git.remote().split(' ') - remotes.size.should == 1 - remotes.include?('origin').should == true - repo.config['user.name'].should ==user.name - repo.config['user.email'].should ==user.email + expect(remotes.size).to eq(1) + expect(remotes.include?('origin')).to eq(true) + expect(repo.config['user.name']).to eq(user.name) + expect(repo.config['user.email']).to eq(user.email) end end @@ -61,16 +61,16 @@ describe 'Gitlab::Satellite::Action' do #set assumptions FileUtils.rm_f(project.satellite.lock_file) - File.exists?(project.satellite.lock_file).should be_false + expect(File.exists?(project.satellite.lock_file)).to be_falsey satellite_action = Gitlab::Satellite::Action.new(user, project) satellite_action.send(:in_locked_and_timed_satellite) do |sat_repo| - repo.should == sat_repo - (File.exists? project.satellite.lock_file).should be_true + expect(repo).to eq(sat_repo) + expect(File.exists? project.satellite.lock_file).to be_truthy called = true end - called.should be_true + expect(called).to be_truthy end @@ -80,19 +80,19 @@ describe 'Gitlab::Satellite::Action' do # Set base assumptions if File.exists? project.satellite.lock_file - FileLockStatusChecker.new(project.satellite.lock_file).flocked?.should be_false + expect(FileLockStatusChecker.new(project.satellite.lock_file).flocked?).to be_falsey end satellite_action = Gitlab::Satellite::Action.new(user, project) satellite_action.send(:in_locked_and_timed_satellite) do |sat_repo| called = true - repo.should == sat_repo - (File.exists? project.satellite.lock_file).should be_true - FileLockStatusChecker.new(project.satellite.lock_file).flocked?.should be_true + expect(repo).to eq(sat_repo) + expect(File.exists? project.satellite.lock_file).to be_truthy + expect(FileLockStatusChecker.new(project.satellite.lock_file).flocked?).to be_truthy end - called.should be_true - FileLockStatusChecker.new(project.satellite.lock_file).flocked?.should be_false + expect(called).to be_truthy + expect(FileLockStatusChecker.new(project.satellite.lock_file).flocked?).to be_falsey end diff --git a/spec/lib/gitlab/satellite/merge_action_spec.rb b/spec/lib/gitlab/satellite/merge_action_spec.rb index 479a73a1081..5cc8b0f21fb 100644 --- a/spec/lib/gitlab/satellite/merge_action_spec.rb +++ b/spec/lib/gitlab/satellite/merge_action_spec.rb @@ -13,9 +13,9 @@ describe 'Gitlab::Satellite::MergeAction' do describe '#commits_between' do def verify_commits(commits, first_commit_sha, last_commit_sha) - commits.each { |commit| commit.class.should == Gitlab::Git::Commit } - commits.first.id.should == first_commit_sha - commits.last.id.should == last_commit_sha + commits.each { |commit| expect(commit.class).to eq(Gitlab::Git::Commit) } + expect(commits.first.id).to eq(first_commit_sha) + expect(commits.last.id).to eq(last_commit_sha) end context 'on fork' do @@ -27,7 +27,7 @@ describe 'Gitlab::Satellite::MergeAction' do context 'between branches' do it 'should raise exception -- not expected to be used by non forks' do - expect { Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request).commits_between }.to raise_error + expect { Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request).commits_between }.to raise_error(RuntimeError) end end end @@ -35,7 +35,7 @@ describe 'Gitlab::Satellite::MergeAction' do describe '#format_patch' do def verify_content(patch) sample_compare.commits.each do |commit| - patch.include?(commit).should be_true + expect(patch.include?(commit)).to be_truthy end end @@ -57,11 +57,11 @@ describe 'Gitlab::Satellite::MergeAction' do describe '#diffs_between_satellite tested against diff_in_satellite' do def is_a_matching_diff(diff, diffs) diff_count = diff.scan('diff --git').size - diff_count.should >= 1 - diffs.size.should == diff_count + expect(diff_count).to be >= 1 + expect(diffs.size).to eq(diff_count) diffs.each do |a_diff| - a_diff.class.should == Gitlab::Git::Diff - (diff.include? a_diff.diff).should be_true + expect(a_diff.class).to eq(Gitlab::Git::Diff) + expect(diff.include? a_diff.diff).to be_truthy end end @@ -75,30 +75,30 @@ describe 'Gitlab::Satellite::MergeAction' do context 'between branches' do it 'should get proper diffs' do - expect{ Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request).diffs_between_satellite }.to raise_error + expect{ Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request).diffs_between_satellite }.to raise_error(RuntimeError) end end end describe '#can_be_merged?' do context 'on fork' do - it { Gitlab::Satellite::MergeAction.new( + it { expect(Gitlab::Satellite::MergeAction.new( merge_request_fork.author, - merge_request_fork).can_be_merged?.should be_true } + merge_request_fork).can_be_merged?).to be_truthy } - it { Gitlab::Satellite::MergeAction.new( + it { expect(Gitlab::Satellite::MergeAction.new( merge_request_fork_with_conflict.author, - merge_request_fork_with_conflict).can_be_merged?.should be_false } + merge_request_fork_with_conflict).can_be_merged?).to be_falsey } end context 'between branches' do - it { Gitlab::Satellite::MergeAction.new( + it { expect(Gitlab::Satellite::MergeAction.new( merge_request.author, - merge_request).can_be_merged?.should be_true } + merge_request).can_be_merged?).to be_truthy } - it { Gitlab::Satellite::MergeAction.new( + it { expect(Gitlab::Satellite::MergeAction.new( merge_request_with_conflict.author, - merge_request_with_conflict).can_be_merged?.should be_false } + merge_request_with_conflict).can_be_merged?).to be_falsey } end end end diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb new file mode 100644 index 00000000000..9c6c3fd8104 --- /dev/null +++ b/spec/lib/gitlab/themes_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Gitlab::Themes do + describe '.body_classes' do + it 'returns a space-separated list of class names' do + css = described_class.body_classes + + expect(css).to include('ui_graphite') + expect(css).to include(' ui_charcoal ') + expect(css).to include(' ui_blue') + end + end + + describe '.by_id' do + it 'returns a Theme by its ID' do + expect(described_class.by_id(1).name).to eq 'Graphite' + expect(described_class.by_id(6).name).to eq 'Blue' + end + end + + describe '.default' do + it 'returns the default application theme' do + allow(described_class).to receive(:default_id).and_return(2) + expect(described_class.default.id).to eq 2 + end + + it 'prevents an infinite loop when configuration default is invalid' do + default = described_class::APPLICATION_DEFAULT + themes = described_class::THEMES + + config = double(default_theme: 0).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + + config = double(default_theme: themes.size + 5).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + end + end + + describe '.each' do + it 'passes the block to the THEMES Array' do + ids = [] + described_class.each { |theme| ids << theme.id } + expect(ids).not_to be_empty + + # TODO (rspeicher): RSpec 3.x + # expect(described_class.each).to yield_with_arg(described_class::Theme) + end + end +end diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb index 2b254d6b3a6..8df84665e16 100644 --- a/spec/lib/gitlab/upgrader_spec.rb +++ b/spec/lib/gitlab/upgrader_spec.rb @@ -5,20 +5,35 @@ describe Gitlab::Upgrader do let(:current_version) { Gitlab::VERSION } describe 'current_version_raw' do - it { upgrader.current_version_raw.should == current_version } + it { expect(upgrader.current_version_raw).to eq(current_version) } end describe 'latest_version?' do it 'should be true if newest version' do - upgrader.stub(latest_version_raw: current_version) - upgrader.latest_version?.should be_true + allow(upgrader).to receive(:latest_version_raw).and_return(current_version) + expect(upgrader.latest_version?).to be_truthy end end describe 'latest_version_raw' do it 'should be latest version for GitLab 5' do - upgrader.stub(current_version_raw: "5.3.0") - upgrader.latest_version_raw.should == "v5.4.2" + allow(upgrader).to receive(:current_version_raw).and_return("5.3.0") + expect(upgrader.latest_version_raw).to eq("v5.4.2") + end + + it 'should get the latest version from tags' do + allow(upgrader).to receive(:fetch_git_tags).and_return([ + '6f0733310546402c15d3ae6128a95052f6c8ea96 refs/tags/v7.1.1', + 'facfec4b242ce151af224e20715d58e628aa5e74 refs/tags/v7.1.1^{}', + 'f7068d99c79cf79befbd388030c051bb4b5e86d4 refs/tags/v7.10.4', + '337225a4fcfa9674e2528cb6d41c46556bba9dfa refs/tags/v7.10.4^{}', + '880e0ba0adbed95d087f61a9a17515e518fc6440 refs/tags/v7.11.1', + '6584346b604f981f00af8011cd95472b2776d912 refs/tags/v7.11.1^{}', + '43af3e65a486a9237f29f56d96c3b3da59c24ae0 refs/tags/v7.11.2', + 'dac18e7728013a77410e926a1e64225703754a2d refs/tags/v7.11.2^{}', + '0bf21fd4b46c980c26fd8c90a14b86a4d90cc950 refs/tags/v7.9.4', + 'b10de29edbaff7219547dc506cb1468ee35065c3 refs/tags/v7.9.4^{}']) + expect(upgrader.latest_version_raw).to eq("v7.11.2") end end end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index eb47bee8336..5153ed15af3 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -5,7 +5,73 @@ describe Gitlab::UrlBuilder do it 'returns the issue url' do issue = create(:issue) url = Gitlab::UrlBuilder.new(:issue).build(issue.id) - expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.to_param}/issues/#{issue.iid}" + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}" + end + end + + describe 'When asking for an merge request' do + it 'returns the merge request url' do + merge_request = create(:merge_request) + url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" + end + end + + describe 'When asking for a note on commit' do + let(:note) { create(:note_on_commit) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end + + describe 'When asking for a note on commit diff' do + let(:note) { create(:note_on_commit_diff) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end + + describe 'When asking for a note on issue' do + let(:issue) { create(:issue) } + let(:note) { create(:note_on_issue, noteable_id: issue.id) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}" + end + end + + describe 'When asking for a note on merge request' do + let(:merge_request) { create(:merge_request) } + let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + describe 'When asking for a note on merge request diff' do + let(:merge_request) { create(:merge_request) } + let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + describe 'When asking for a note on project snippet' do + let(:snippet) { create(:project_snippet) } + let(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" end end end diff --git a/spec/lib/gitlab/version_info_spec.rb b/spec/lib/gitlab/version_info_spec.rb index 94dccf7a4e5..5afeb1c1ec3 100644 --- a/spec/lib/gitlab/version_info_spec.rb +++ b/spec/lib/gitlab/version_info_spec.rb @@ -12,58 +12,58 @@ describe 'Gitlab::VersionInfo', no_db: true do end context '>' do - it { @v2_0_0.should > @v1_1_0 } - it { @v1_1_0.should > @v1_0_1 } - it { @v1_0_1.should > @v1_0_0 } - it { @v1_0_0.should > @v0_1_0 } - it { @v0_1_0.should > @v0_0_1 } + it { expect(@v2_0_0).to be > @v1_1_0 } + it { expect(@v1_1_0).to be > @v1_0_1 } + it { expect(@v1_0_1).to be > @v1_0_0 } + it { expect(@v1_0_0).to be > @v0_1_0 } + it { expect(@v0_1_0).to be > @v0_0_1 } end context '>=' do - it { @v2_0_0.should >= Gitlab::VersionInfo.new(2, 0, 0) } - it { @v2_0_0.should >= @v1_1_0 } + it { expect(@v2_0_0).to be >= Gitlab::VersionInfo.new(2, 0, 0) } + it { expect(@v2_0_0).to be >= @v1_1_0 } end context '<' do - it { @v0_0_1.should < @v0_1_0 } - it { @v0_1_0.should < @v1_0_0 } - it { @v1_0_0.should < @v1_0_1 } - it { @v1_0_1.should < @v1_1_0 } - it { @v1_1_0.should < @v2_0_0 } + it { expect(@v0_0_1).to be < @v0_1_0 } + it { expect(@v0_1_0).to be < @v1_0_0 } + it { expect(@v1_0_0).to be < @v1_0_1 } + it { expect(@v1_0_1).to be < @v1_1_0 } + it { expect(@v1_1_0).to be < @v2_0_0 } end context '<=' do - it { @v0_0_1.should <= Gitlab::VersionInfo.new(0, 0, 1) } - it { @v0_0_1.should <= @v0_1_0 } + it { expect(@v0_0_1).to be <= Gitlab::VersionInfo.new(0, 0, 1) } + it { expect(@v0_0_1).to be <= @v0_1_0 } end context '==' do - it { @v0_0_1.should == Gitlab::VersionInfo.new(0, 0, 1) } - it { @v0_1_0.should == Gitlab::VersionInfo.new(0, 1, 0) } - it { @v1_0_0.should == Gitlab::VersionInfo.new(1, 0, 0) } + it { expect(@v0_0_1).to eq(Gitlab::VersionInfo.new(0, 0, 1)) } + it { expect(@v0_1_0).to eq(Gitlab::VersionInfo.new(0, 1, 0)) } + it { expect(@v1_0_0).to eq(Gitlab::VersionInfo.new(1, 0, 0)) } end context '!=' do - it { @v0_0_1.should_not == @v0_1_0 } + it { expect(@v0_0_1).not_to eq(@v0_1_0) } end context 'unknown' do - it { @unknown.should_not be @v0_0_1 } - it { @unknown.should_not be Gitlab::VersionInfo.new } + it { expect(@unknown).not_to be @v0_0_1 } + it { expect(@unknown).not_to be Gitlab::VersionInfo.new } it { expect{@unknown > @v0_0_1}.to raise_error(ArgumentError) } it { expect{@unknown < @v0_0_1}.to raise_error(ArgumentError) } end context 'parse' do - it { Gitlab::VersionInfo.parse("1.0.0").should == @v1_0_0 } - it { Gitlab::VersionInfo.parse("1.0.0.1").should == @v1_0_0 } - it { Gitlab::VersionInfo.parse("git 1.0.0b1").should == @v1_0_0 } - it { Gitlab::VersionInfo.parse("git 1.0b1").should_not be_valid } + it { expect(Gitlab::VersionInfo.parse("1.0.0")).to eq(@v1_0_0) } + it { expect(Gitlab::VersionInfo.parse("1.0.0.1")).to eq(@v1_0_0) } + it { expect(Gitlab::VersionInfo.parse("git 1.0.0b1")).to eq(@v1_0_0) } + it { expect(Gitlab::VersionInfo.parse("git 1.0b1")).not_to be_valid } end context 'to_s' do - it { @v1_0_0.to_s.should == "1.0.0" } - it { @unknown.to_s.should == "Unknown" } + it { expect(@v1_0_0.to_s).to eq("1.0.0") } + it { expect(@unknown.to_s).to eq("Unknown") } end end diff --git a/spec/lib/repository_cache_spec.rb b/spec/lib/repository_cache_spec.rb new file mode 100644 index 00000000000..37240d51310 --- /dev/null +++ b/spec/lib/repository_cache_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../lib/repository_cache' + +describe RepositoryCache do + let(:backend) { double('backend').as_null_object } + let(:cache) { RepositoryCache.new('example', backend) } + + describe '#cache_key' do + it 'includes the namespace' do + expect(cache.cache_key(:foo)).to eq 'foo:example' + end + end + + describe '#expire' do + it 'expires the given key from the cache' do + cache.expire(:foo) + expect(backend).to have_received(:delete).with('foo:example') + end + end + + describe '#fetch' do + it 'fetches the given key from the cache' do + cache.fetch(:bar) + expect(backend).to have_received(:fetch).with('bar:example') + end + + it 'accepts a block' do + p = -> {} + + cache.fetch(:baz, &p) + expect(backend).to have_received(:fetch).with('baz:example', &p) + end + end +end diff --git a/spec/lib/votes_spec.rb b/spec/lib/votes_spec.rb index a3c353d5eab..df243a26008 100644 --- a/spec/lib/votes_spec.rb +++ b/spec/lib/votes_spec.rb @@ -5,132 +5,181 @@ describe Issue, 'Votes' do describe "#upvotes" do it "with no notes has a 0/0 score" do - issue.upvotes.should == 0 + expect(issue.upvotes).to eq(0) end it "should recognize non-+1 notes" do add_note "No +1 here" - issue.should have(1).note - issue.notes.first.upvote?.should be_false - issue.upvotes.should == 0 + expect(issue.notes.size).to eq(1) + expect(issue.notes.first.upvote?).to be_falsey + expect(issue.upvotes).to eq(0) end it "should recognize a single +1 note" do add_note "+1 This is awesome" - issue.upvotes.should == 1 + expect(issue.upvotes).to eq(1) end - it "should recognize multiple +1 notes" do - add_note "+1 This is awesome" - add_note "+1 I want this" - issue.upvotes.should == 2 + it 'should recognize multiple +1 notes' do + add_note '+1 This is awesome', create(:user) + add_note '+1 I want this', create(:user) + expect(issue.upvotes).to eq(2) + end + + it 'should not count 2 +1 votes from the same user' do + add_note '+1 This is awesome' + add_note '+1 I want this' + expect(issue.upvotes).to eq(1) end end describe "#downvotes" do it "with no notes has a 0/0 score" do - issue.downvotes.should == 0 + expect(issue.downvotes).to eq(0) end it "should recognize non--1 notes" do add_note "Almost got a -1" - issue.should have(1).note - issue.notes.first.downvote?.should be_false - issue.downvotes.should == 0 + expect(issue.notes.size).to eq(1) + expect(issue.notes.first.downvote?).to be_falsey + expect(issue.downvotes).to eq(0) end it "should recognize a single -1 note" do add_note "-1 This is bad" - issue.downvotes.should == 1 + expect(issue.downvotes).to eq(1) end it "should recognize multiple -1 notes" do - add_note "-1 This is bad" - add_note "-1 Away with this" - issue.downvotes.should == 2 + add_note('-1 This is bad', create(:user)) + add_note('-1 Away with this', create(:user)) + expect(issue.downvotes).to eq(2) end end describe "#votes_count" do it "with no notes has a 0/0 score" do - issue.votes_count.should == 0 + expect(issue.votes_count).to eq(0) end it "should recognize non notes" do add_note "No +1 here" - issue.should have(1).note - issue.votes_count.should == 0 + expect(issue.notes.size).to eq(1) + expect(issue.votes_count).to eq(0) end it "should recognize a single +1 note" do add_note "+1 This is awesome" - issue.votes_count.should == 1 + expect(issue.votes_count).to eq(1) end it "should recognize a single -1 note" do add_note "-1 This is bad" - issue.votes_count.should == 1 + expect(issue.votes_count).to eq(1) end it "should recognize multiple notes" do - add_note "+1 This is awesome" - add_note "-1 This is bad" - add_note "+1 I want this" - issue.votes_count.should == 3 + add_note('+1 This is awesome', create(:user)) + add_note('-1 This is bad', create(:user)) + add_note('+1 I want this', create(:user)) + expect(issue.votes_count).to eq(3) + end + + it 'should not count 2 -1 votes from the same user' do + add_note '-1 This is suspicious' + add_note '-1 This is bad' + expect(issue.votes_count).to eq(1) end end describe "#upvotes_in_percent" do it "with no notes has a 0% score" do - issue.upvotes_in_percent.should == 0 + expect(issue.upvotes_in_percent).to eq(0) end it "should count a single 1 note as 100%" do add_note "+1 This is awesome" - issue.upvotes_in_percent.should == 100 + expect(issue.upvotes_in_percent).to eq(100) end - it "should count multiple +1 notes as 100%" do - add_note "+1 This is awesome" - add_note "+1 I want this" - issue.upvotes_in_percent.should == 100 + it 'should count multiple +1 notes as 100%' do + add_note('+1 This is awesome', create(:user)) + add_note('+1 I want this', create(:user)) + expect(issue.upvotes_in_percent).to eq(100) end - it "should count fractions for multiple +1 and -1 notes correctly" do - add_note "+1 This is awesome" - add_note "+1 I want this" - add_note "-1 This is bad" - add_note "+1 me too" - issue.upvotes_in_percent.should == 75 + it 'should count fractions for multiple +1 and -1 notes correctly' do + add_note('+1 This is awesome', create(:user)) + add_note('+1 I want this', create(:user)) + add_note('-1 This is bad', create(:user)) + add_note('+1 me too', create(:user)) + expect(issue.upvotes_in_percent).to eq(75) end end describe "#downvotes_in_percent" do it "with no notes has a 0% score" do - issue.downvotes_in_percent.should == 0 + expect(issue.downvotes_in_percent).to eq(0) end it "should count a single -1 note as 100%" do add_note "-1 This is bad" - issue.downvotes_in_percent.should == 100 + expect(issue.downvotes_in_percent).to eq(100) end - it "should count multiple -1 notes as 100%" do - add_note "-1 This is bad" - add_note "-1 Away with this" - issue.downvotes_in_percent.should == 100 + it 'should count multiple -1 notes as 100%' do + add_note('-1 This is bad', create(:user)) + add_note('-1 Away with this', create(:user)) + expect(issue.downvotes_in_percent).to eq(100) end - it "should count fractions for multiple +1 and -1 notes correctly" do - add_note "+1 This is awesome" - add_note "+1 I want this" - add_note "-1 This is bad" - add_note "+1 me too" - issue.downvotes_in_percent.should == 25 + it 'should count fractions for multiple +1 and -1 notes correctly' do + add_note('+1 This is awesome', create(:user)) + add_note('+1 I want this', create(:user)) + add_note('-1 This is bad', create(:user)) + add_note('+1 me too', create(:user)) + expect(issue.downvotes_in_percent).to eq(25) + end + end + + describe '#filter_superceded_votes' do + + it 'should count a users vote only once amongst multiple votes' do + add_note('-1 This needs work before I will accept it') + add_note('+1 I want this', create(:user)) + add_note('+1 This is is awesome', create(:user)) + add_note('+1 this looks good now') + add_note('+1 This is awesome', create(:user)) + add_note('+1 me too', create(:user)) + expect(issue.downvotes).to eq(0) + expect(issue.upvotes).to eq(5) end + + it 'should count each users vote only once' do + add_note '-1 This needs work before it will be accepted' + add_note '+1 I like this' + add_note '+1 I still like this' + add_note '+1 I really like this' + add_note '+1 Give me this now!!!!' + expect(issue.downvotes).to eq(0) + expect(issue.upvotes).to eq(1) + end + + it 'should count a users vote only once without caring about comments' do + add_note '-1 This needs work before it will be accepted' + add_note 'Comment 1' + add_note 'Another comment' + add_note '+1 vote' + add_note 'final comment' + expect(issue.downvotes).to eq(0) + expect(issue.upvotes).to eq(1) + end + end - def add_note(text) - issue.notes << create(:note, note: text, project: issue.project) + def add_note(text, author = issue.author) + created_at = Time.now - 1.hour + Note.count.seconds + issue.notes << create(:note, note: text, project: issue.project, + author_id: author.id, created_at: created_at) end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index a0c37587b23..89853d05161 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -1,110 +1,122 @@ require 'spec_helper' +require 'email_spec' describe Notify do include EmailSpec::Helpers include EmailSpec::Matchers include RepoHelpers + new_user_address = 'newguy@example.com' + + let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name } let(:gitlab_sender) { Gitlab.config.gitlab.email_from } + let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to } let(:recipient) { create(:user, email: 'recipient@example.com') } let(:project) { create(:project) } + before(:each) do + ActionMailer::Base.deliveries.clear + email = recipient.emails.create(email: "notifications@example.com") + recipient.update_attribute(:notification_email, email.email) + end + shared_examples 'a multiple recipients email' do it 'is sent to the given recipient' do - should deliver_to recipient.email + is_expected.to deliver_to recipient.notification_email end end shared_examples 'an email sent from GitLab' do it 'is sent from GitLab' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq('GitLab') - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(gitlab_sender_display_name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'has a Reply-To address' do + reply_to = subject.header[:reply_to].addresses + expect(reply_to).to eq([gitlab_sender_reply_to]) end end shared_examples 'an email starting a new thread' do |message_id_prefix| it 'has a discussion identifier' do - should have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ end end shared_examples 'an answer to an existing thread' do |thread_id_prefix| it 'has a subject that begins with Re: ' do - should have_subject /^Re: / + is_expected.to have_subject /^Re: / end it 'has headers that reference an existing thread' do - should have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - should have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ end end - describe 'for new users, the email' do - let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: 'newguy@example.com', created_by_id: 1) } - - token = 'kETLwRaayvigPq_x3SNM' - - subject { Notify.new_user_email(new_user.id, token) } - - it_behaves_like 'an email sent from GitLab' - + shared_examples 'a new user email' do |user_email, site_path| it 'is sent to the new user' do - should deliver_to new_user.email + is_expected.to deliver_to user_email end it 'has the correct subject' do - should have_subject /^Account was created for you$/i + is_expected.to have_subject /^Account was created for you$/i end it 'contains the new user\'s login name' do - should have_body_text /#{new_user.email}/ + is_expected.to have_body_text /#{user_email}/ end + it 'includes a link to the site' do + is_expected.to have_body_text /#{site_path}/ + end + end + + describe 'for new users, the email' do + let(:example_site_path) { root_path } + let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } + + token = 'kETLwRaayvigPq_x3SNM' + + subject { Notify.new_user_email(new_user.id, token) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'a new user email', new_user_address + it 'contains the password text' do - should have_body_text /Click here to set your password/ + is_expected.to have_body_text /Click here to set your password/ end it 'includes a link for user to set password' do params = "reset_password_token=#{token}" - should have_body_text( + is_expected.to have_body_text( %r{http://localhost(:\d+)?/users/password/edit\?#{params}} ) end - it 'includes a link to the site' do - should have_body_text /#{example_site_path}/ + it 'explains the reset link expiration' do + is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/) + is_expected.to have_body_text(new_user_password_url) + is_expected.to have_body_text(/\?user_email=.*%40.*/) end end describe 'for users that signed up, the email' do let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: 'newguy@example.com', password: "securePassword") } + let(:new_user) { create(:user, email: new_user_address, password: "securePassword") } subject { Notify.new_user_email(new_user.id) } it_behaves_like 'an email sent from GitLab' - - it 'is sent to the new user' do - should deliver_to new_user.email - end - - it 'has the correct subject' do - should have_subject /^Account was created for you$/i - end - - it 'contains the new user\'s login name' do - should have_body_text /#{new_user.email}/ - end + it_behaves_like 'a new user email', new_user_address it 'should not contain the new user\'s password' do - should_not have_body_text /password/ - end - - it 'includes a link to the site' do - should have_body_text /#{example_site_path}/ + is_expected.not_to have_body_text /password/ end end @@ -116,19 +128,19 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it 'is sent to the new user' do - should deliver_to key.user.email + is_expected.to deliver_to key.user.email end it 'has the correct subject' do - should have_subject /^SSH key was added to your account$/i + is_expected.to have_subject /^SSH key was added to your account$/i end it 'contains the new ssh key title' do - should have_body_text /#{key.title}/ + is_expected.to have_body_text /#{key.title}/ end it 'includes a link to ssh keys page' do - should have_body_text /#{profile_keys_path}/ + is_expected.to have_body_text /#{profile_keys_path}/ end end @@ -138,19 +150,19 @@ describe Notify do subject { Notify.new_email_email(email.id) } it 'is sent to the new user' do - should deliver_to email.user.email + is_expected.to deliver_to email.user.email end it 'has the correct subject' do - should have_subject /^Email was added to your account$/i + is_expected.to have_subject /^Email was added to your account$/i end it 'contains the new email address' do - should have_body_text /#{email.email}/ + is_expected.to have_body_text /#{email.email}/ end it 'includes a link to emails page' do - should have_body_text /#{profile_emails_path}/ + is_expected.to have_body_text /#{profile_emails_path}/ end end @@ -163,18 +175,18 @@ describe Notify do shared_examples 'an assignee email' do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(current_user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) end it 'is sent to the assignee' do - should deliver_to assignee.email + is_expected.to deliver_to assignee.email end end context 'for issues' do let(:issue) { create(:issue, author: current_user, assignee: assignee, project: project) } - let(:issue_with_description) { create(:issue, author: current_user, assignee: assignee, project: project, description: Faker::Lorem.sentence) } + let(:issue_with_description) { create(:issue, author: current_user, assignee: assignee, project: project, description: FFaker::Lorem.sentence) } describe 'that are new' do subject { Notify.new_issue_email(issue.assignee_id, issue.id) } @@ -183,11 +195,11 @@ describe Notify do it_behaves_like 'an email starting a new thread', 'issue' it 'has the correct subject' do - should have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ + is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ end it 'contains a link to the new issue' do - should have_body_text /#{project_issue_path project, issue}/ + is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end end @@ -195,7 +207,7 @@ describe Notify do subject { Notify.new_issue_email(issue_with_description.assignee_id, issue_with_description.id) } it 'contains the description' do - should have_body_text /#{issue_with_description.description}/ + is_expected.to have_body_text /#{issue_with_description.description}/ end end @@ -207,24 +219,24 @@ describe Notify do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(current_user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) end it 'has the correct subject' do - should have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ end it 'contains the name of the previous assignee' do - should have_body_text /#{previous_assignee.name}/ + is_expected.to have_body_text /#{previous_assignee.name}/ end it 'contains the name of the new assignee' do - should have_body_text /#{assignee.name}/ + is_expected.to have_body_text /#{assignee.name}/ end it 'contains a link to the issue' do - should have_body_text /#{project_issue_path project, issue}/ + is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end end @@ -236,24 +248,24 @@ describe Notify do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(current_user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) end it 'has the correct subject' do - should have_subject /#{issue.title} \(##{issue.iid}\)/i + is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i end it 'contains the new status' do - should have_body_text /#{status}/i + is_expected.to have_body_text /#{status}/i end it 'contains the user name' do - should have_body_text /#{current_user.name}/i + is_expected.to have_body_text /#{current_user.name}/i end it 'contains a link to the issue' do - should have_body_text /#{project_issue_path project, issue}/ + is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end end @@ -262,7 +274,7 @@ describe Notify do context 'for merge requests' do let(:merge_author) { create(:user) } let(:merge_request) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project) } - let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: Faker::Lorem.sentence) } + let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: FFaker::Lorem.sentence) } describe 'that are new' do subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) } @@ -271,23 +283,23 @@ describe Notify do it_behaves_like 'an email starting a new thread', 'merge_request' it 'has the correct subject' do - should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ end it 'contains a link to the new merge request' do - should have_body_text /#{project_merge_request_path(project, merge_request)}/ + is_expected.to have_body_text /#{namespace_project_merge_request_path(project.namespace, project, merge_request)}/ end it 'contains the source branch for the merge request' do - should have_body_text /#{merge_request.source_branch}/ + is_expected.to have_body_text /#{merge_request.source_branch}/ end it 'contains the target branch for the merge request' do - should have_body_text /#{merge_request.target_branch}/ + is_expected.to have_body_text /#{merge_request.target_branch}/ end it 'has the correct message-id set' do - should have_header 'Message-ID', "<merge_request_#{merge_request.id}@#{Gitlab.config.gitlab.host}>" + is_expected.to have_header 'Message-ID', "<merge_request_#{merge_request.id}@#{Gitlab.config.gitlab.host}>" end end @@ -295,7 +307,7 @@ describe Notify do subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) } it 'contains the description' do - should have_body_text /#{merge_request_with_description.description}/ + is_expected.to have_body_text /#{merge_request_with_description.description}/ end end @@ -307,24 +319,24 @@ describe Notify do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(current_user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) end it 'has the correct subject' do - should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ end it 'contains the name of the previous assignee' do - should have_body_text /#{previous_assignee.name}/ + is_expected.to have_body_text /#{previous_assignee.name}/ end it 'contains the name of the new assignee' do - should have_body_text /#{assignee.name}/ + is_expected.to have_body_text /#{assignee.name}/ end it 'contains a link to the merge request' do - should have_body_text /#{project_merge_request_path project, merge_request}/ + is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ end end @@ -336,24 +348,24 @@ describe Notify do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(current_user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) end it 'has the correct subject' do - should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/i + is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/i end it 'contains the new status' do - should have_body_text /#{status}/i + is_expected.to have_body_text /#{status}/i end it 'contains the user name' do - should have_body_text /#{current_user.name}/i + is_expected.to have_body_text /#{current_user.name}/i end it 'contains a link to the merge request' do - should have_body_text /#{project_merge_request_path project, merge_request}/ + is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ end end @@ -365,20 +377,20 @@ describe Notify do it 'is sent as the merge author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(merge_author.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(merge_author.name) + expect(sender.address).to eq(gitlab_sender) end it 'has the correct subject' do - should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ end it 'contains the new status' do - should have_body_text /merged/i + is_expected.to have_body_text /merged/i end it 'contains a link to the merge request' do - should have_body_text /#{project_merge_request_path project, merge_request}/ + is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ end end end @@ -392,15 +404,15 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it 'has the correct subject' do - should have_subject /Project was moved/ + is_expected.to have_subject /Project was moved/ end it 'contains name of project' do - should have_body_text /#{project.name_with_namespace}/ + is_expected.to have_body_text /#{project.name_with_namespace}/ end it 'contains new user role' do - should have_body_text /#{project.ssh_url_to_repo}/ + is_expected.to have_body_text /#{project.ssh_url_to_repo}/ end end @@ -415,13 +427,13 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it 'has the correct subject' do - should have_subject /Access to project was granted/ + is_expected.to have_subject /Access to project was granted/ end it 'contains name of project' do - should have_body_text /#{project.name}/ + is_expected.to have_body_text /#{project.name}/ end it 'contains new user role' do - should have_body_text /#{project_member.human_access}/ + is_expected.to have_body_text /#{project_member.human_access}/ end end @@ -430,29 +442,29 @@ describe Notify do let(:note) { create(:note, project: project, author: note_author) } before :each do - Note.stub(:find).with(note.id).and_return(note) + allow(Note).to receive(:find).with(note.id).and_return(note) end shared_examples 'a note email' do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(note_author.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(note_author.name) + expect(sender.address).to eq(gitlab_sender) end it 'is sent to the given recipient' do - should deliver_to recipient.email + is_expected.to deliver_to recipient.notification_email end it 'contains the message from the note' do - should have_body_text /#{note.note}/ + is_expected.to have_body_text /#{note.note}/ end end describe 'on a commit' do - let(:commit) { project.repository.commit } + let(:commit) { project.commit } - before(:each) { note.stub(:noteable).and_return(commit) } + before(:each) { allow(note).to receive(:noteable).and_return(commit) } subject { Notify.note_commit_email(recipient.id, note.id) } @@ -460,18 +472,18 @@ describe Notify do it_behaves_like 'an answer to an existing thread', 'commits' it 'has the correct subject' do - should have_subject /#{commit.title} \(#{commit.short_id}\)/ + is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ end it 'contains a link to the commit' do - should have_body_text commit.short_id + is_expected.to have_body_text commit.short_id end end describe 'on a merge request' do let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - let(:note_on_merge_request_path) { project_merge_request_path(project, merge_request, anchor: "note_#{note.id}") } - before(:each) { note.stub(:noteable).and_return(merge_request) } + let(:note_on_merge_request_path) { namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: "note_#{note.id}") } + before(:each) { allow(note).to receive(:noteable).and_return(merge_request) } subject { Notify.note_merge_request_email(recipient.id, note.id) } @@ -479,18 +491,18 @@ describe Notify do it_behaves_like 'an answer to an existing thread', 'merge_request' it 'has the correct subject' do - should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ end it 'contains a link to the merge request note' do - should have_body_text /#{note_on_merge_request_path}/ + is_expected.to have_body_text /#{note_on_merge_request_path}/ end end describe 'on an issue' do let(:issue) { create(:issue, project: project) } - let(:note_on_issue_path) { project_issue_path(project, issue, anchor: "note_#{note.id}") } - before(:each) { note.stub(:noteable).and_return(issue) } + let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") } + before(:each) { allow(note).to receive(:noteable).and_return(issue) } subject { Notify.note_issue_email(recipient.id, note.id) } @@ -498,11 +510,11 @@ describe Notify do it_behaves_like 'an answer to an existing thread', 'issue' it 'has the correct subject' do - should have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ end it 'contains a link to the issue note' do - should have_body_text /#{note_on_issue_path}/ + is_expected.to have_body_text /#{note_on_issue_path}/ end end end @@ -518,15 +530,15 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it 'has the correct subject' do - should have_subject /Access to group was granted/ + is_expected.to have_subject /Access to group was granted/ end it 'contains name of project' do - should have_body_text /#{group.name}/ + is_expected.to have_body_text /#{group.name}/ end it 'contains new user role' do - should have_body_text /#{membership.human_access}/ + is_expected.to have_body_text /#{membership.human_access}/ end end @@ -544,15 +556,109 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it 'is sent to the new user' do - should deliver_to 'new-email@mail.com' + is_expected.to deliver_to 'new-email@mail.com' end it 'has the correct subject' do - should have_subject "Confirmation instructions" + is_expected.to have_subject "Confirmation instructions" end it 'includes a link to the site' do - should have_body_text /#{example_site_path}/ + is_expected.to have_body_text /#{example_site_path}/ + end + end + + describe 'email on push for a created branch' do + let(:example_site_path) { root_path } + 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) } + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(user.name) + 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 + + it 'contains a link to the branch' do + is_expected.to have_body_text /#{tree_path}/ + end + end + + describe 'email on push for a created tag' do + let(:example_site_path) { root_path } + 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) } + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(user.name) + 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 + + it 'contains a link to the tag' do + is_expected.to have_body_text /#{tree_path}/ + end + end + + describe 'email on push for a deleted branch' 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) } + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(user.name) + 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 + end + + describe 'email on push for a deleted tag' 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) } + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(user.name) + 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 end @@ -560,35 +666,103 @@ describe Notify do let(:example_site_path) { root_path } let(:user) { create(:user) } let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) } - let(:commits) { Commit.decorate(compare.commits) } - let(:diff_path) { project_compare_path(project, from: commits.first, to: commits.last) } + 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 } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(user.name) + expect(sender.address).to eq(gitlab_sender) end it 'is sent to recipient' do - should deliver_to 'devs@company.name' + is_expected.to deliver_to 'devs@company.name' end it 'has the correct subject' do - should have_subject /#{commits.length} new commits pushed to repository/ + is_expected.to have_subject /\[#{project.path_with_namespace}\]\[master\] #{commits.length} commits:/ end it 'includes commits list' do - should have_body_text /Change some files/ + is_expected.to have_body_text /Change some files/ end it 'includes diffs' do - should have_body_text /def archive_formats_regex/ + is_expected.to have_body_text /def archive_formats_regex/ end it 'contains a link to the diff' do - should have_body_text /#{diff_path}/ + is_expected.to have_body_text /#{diff_path}/ + end + + it 'doesn not contain the misleading footer' do + is_expected.not_to have_body_text /you are a member of/ + end + + context "when set to send from committer email if domain matches" do + + let(:send_from_committer_email) { true } + + before do + allow(Gitlab.config.gitlab).to receive(:host).and_return("gitlab.corp.company.com") + end + + context "when the committer email domain is within the GitLab domain" do + + before do + user.update_attribute(:email, "user@company.com") + user.confirm! + end + + it "is sent from the committer email" do + sender = subject.header[:from].addrs[0] + expect(sender.address).to eq(user.email) + end + + it "is set to reply to the committer email" do + sender = subject.header[:reply_to].addrs[0] + expect(sender.address).to eq(user.email) + end + end + + context "when the committer email domain is not completely within the GitLab domain" do + + before do + user.update_attribute(:email, "user@something.company.com") + user.confirm! + end + + it "is sent from the default email" do + sender = subject.header[:from].addrs[0] + expect(sender.address).to eq(gitlab_sender) + end + + it "is set to reply to the default email" do + sender = subject.header[:reply_to].addrs[0] + expect(sender.address).to eq(gitlab_sender_reply_to) + end + end + + context "when the committer email domain is outside the GitLab domain" do + + before do + user.update_attribute(:email, "user@mpany.com") + user.confirm! + end + + it "is sent from the default email" do + sender = subject.header[:from].addrs[0] + expect(sender.address).to eq(gitlab_sender) + end + + it "is set to reply to the default email" do + sender = subject.header[:reply_to].addrs[0] + expect(sender.address).to eq(gitlab_sender_reply_to) + end + end end end @@ -596,35 +770,35 @@ describe Notify do let(:example_site_path) { root_path } let(:user) { create(:user) } let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) } - let(:commits) { Commit.decorate(compare.commits) } - let(:diff_path) { project_commit_path(project, commits.first) } + let(:commits) { Commit.decorate(compare.commits, nil) } + let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(user.name) + expect(sender.address).to eq(gitlab_sender) end it 'is sent to recipient' do - should deliver_to 'devs@company.name' + is_expected.to deliver_to 'devs@company.name' end it 'has the correct subject' do - should have_subject /#{commits.first.title}/ + is_expected.to have_subject /#{commits.first.title}/ end it 'includes commits list' do - should have_body_text /Change some files/ + is_expected.to have_body_text /Change some files/ end it 'includes diffs' do - should have_body_text /def archive_formats_regex/ + is_expected.to have_body_text /def archive_formats_regex/ end it 'contains a link to the diff' do - should have_body_text /#{diff_path}/ + is_expected.to have_body_text /#{diff_path}/ end end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb new file mode 100644 index 00000000000..d648f4078be --- /dev/null +++ b/spec/models/application_setting_spec.rb @@ -0,0 +1,52 @@ +# == 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) +# twitter_sharing_enabled :boolean default(TRUE) +# restricted_visibility_levels :text +# max_attachment_size :integer default(10), not null +# session_expire_delay :integer default(10080), not null +# default_project_visibility :integer +# default_snippet_visibility :integer +# restricted_signup_domains :text +# + +require 'spec_helper' + +describe ApplicationSetting, models: true do + it { expect(ApplicationSetting.create_from_defaults).to be_valid } + + context 'restricted signup domains' do + let(:setting) { ApplicationSetting.create_from_defaults } + + it 'set single domain' do + setting.restricted_signup_domains_raw = 'example.com' + expect(setting.restricted_signup_domains).to eq(['example.com']) + end + + it 'set multiple domains with spaces' do + setting.restricted_signup_domains_raw = 'example.com *.example.com' + expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com']) + end + + it 'set multiple domains with newlines and a space' do + setting.restricted_signup_domains_raw = "example.com\n *.example.com" + expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com']) + end + + it 'set multiple domains with commas' do + setting.restricted_signup_domains_raw = "example.com, *.example.com" + expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com']) + end + end +end diff --git a/spec/models/assembla_service_spec.rb b/spec/models/assembla_service_spec.rb deleted file mode 100644 index 4300090eb13..00000000000 --- a/spec/models/assembla_service_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -require 'spec_helper' - -describe AssemblaService, models: true do - describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } - end - - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project) } - - before do - @assembla_service = AssemblaService.new - @assembla_service.stub( - project_id: project.id, - project: project, - service_hook: true, - token: 'verySecret', - subdomain: 'project_name' - ) - @sample_data = GitPushService.new.sample_data(project, user) - @api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret' - WebMock.stub_request(:post, @api_url) - end - - it "should call Assembla API" do - @assembla_service.execute(@sample_data) - WebMock.should have_requested(:post, @api_url).with( - body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/ - ).once - end - end -end diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 0f31c407c90..8ab72151a69 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -18,22 +18,22 @@ require 'spec_helper' describe BroadcastMessage do subject { create(:broadcast_message) } - it { should be_valid } + it { is_expected.to be_valid } describe :current do it "should return last message if time match" do broadcast_message = create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow) - BroadcastMessage.current.should == broadcast_message + expect(BroadcastMessage.current).to eq(broadcast_message) end it "should return nil if time not come" do broadcast_message = create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days) - BroadcastMessage.current.should be_nil + expect(BroadcastMessage.current).to be_nil end it "should return nil if time has passed" do broadcast_message = create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday) - BroadcastMessage.current.should be_nil + expect(BroadcastMessage.current).to be_nil end end end diff --git a/spec/models/buildbox_service_spec.rb b/spec/models/buildbox_service_spec.rb deleted file mode 100644 index 1d9ca51be16..00000000000 --- a/spec/models/buildbox_service_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -require 'spec_helper' - -describe BuildboxService do - describe 'Associations' do - it { should belong_to :project } - it { should have_one :service_hook } - end - - describe 'commits methods' do - before do - @project = Project.new - @project.stub( - default_branch: 'default-brancho' - ) - - @service = BuildboxService.new - @service.stub( - project: @project, - service_hook: true, - project_url: 'https://buildbox.io/account-name/example-project', - token: 'secret-sauce-webhook-token:secret-sauce-status-token' - ) - end - - describe :webhook_url do - it 'returns the webhook url' do - @service.webhook_url.should == - 'https://webhook.buildbox.io/deliver/secret-sauce-webhook-token' - end - end - - describe :commit_status_path do - it 'returns the correct status page' do - @service.commit_status_path('2ab7834c').should == - 'https://gitlab.buildbox.io/status/secret-sauce-status-token.json?commit=2ab7834c' - end - end - - describe :build_page do - it 'returns the correct build page' do - @service.build_page('2ab7834c').should == - 'https://buildbox.io/account-name/example-project/builds?commit=2ab7834c' - end - end - - describe :builds_page do - it 'returns the correct path to the builds page' do - @service.builds_path.should == - 'https://buildbox.io/account-name/example-project/builds?branch=default-brancho' - end - end - - describe :status_img_path do - it 'returns the correct path to the status image' do - @service.status_img_path.should == 'https://badge.buildbox.io/secret-sauce-status-token.svg' - end - end - end -end diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb new file mode 100644 index 00000000000..e3ab4812464 --- /dev/null +++ b/spec/models/commit_range_spec.rb @@ -0,0 +1,143 @@ +require 'spec_helper' + +describe CommitRange do + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Referable) } + end + + let(:sha_from) { 'f3f85602' } + let(:sha_to) { 'e86e1013' } + + let(:range) { described_class.new("#{sha_from}...#{sha_to}") } + let(:range2) { described_class.new("#{sha_from}..#{sha_to}") } + + it 'raises ArgumentError when given an invalid range string' do + expect { described_class.new("Foo") }.to raise_error(ArgumentError) + end + + describe '#to_s' do + it 'is correct for three-dot syntax' do + expect(range.to_s).to eq "#{sha_from[0..7]}...#{sha_to[0..7]}" + end + + it 'is correct for two-dot syntax' do + expect(range2.to_s).to eq "#{sha_from[0..7]}..#{sha_to[0..7]}" + end + end + + describe '#to_reference' do + let(:project) { double('project', to_reference: 'namespace1/project') } + + before do + range.project = project + end + + it 'returns a String reference to the object' do + expect(range.to_reference).to eq range.to_s + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{range.to_s}" + end + end + + describe '#reference_title' do + it 'returns the correct String for three-dot ranges' do + expect(range.reference_title).to eq "Commits #{sha_from} through #{sha_to}" + end + + it 'returns the correct String for two-dot ranges' do + expect(range2.reference_title).to eq "Commits #{sha_from}^ through #{sha_to}" + end + end + + describe '#to_param' do + it 'includes the correct keys' do + expect(range.to_param.keys).to eq %i(from to) + end + + it 'includes the correct values for a three-dot range' do + expect(range.to_param).to eq({from: sha_from, to: sha_to}) + end + + it 'includes the correct values for a two-dot range' do + expect(range2.to_param).to eq({from: sha_from + '^', to: sha_to}) + end + end + + describe '#exclude_start?' do + it 'is false for three-dot ranges' do + expect(range.exclude_start?).to eq false + end + + it 'is true for two-dot ranges' do + expect(range2.exclude_start?).to eq true + end + end + + describe '#valid_commits?' do + context 'without a project' do + it 'returns nil' do + expect(range.valid_commits?).to be_nil + end + end + + it 'accepts an optional project argument' do + project1 = double('project1').as_null_object + project2 = double('project2').as_null_object + + # project1 gets assigned through the accessor, but ignored when not given + # as an argument to `valid_commits?` + expect(project1).not_to receive(:present?) + range.project = project1 + + # project2 gets passed to `valid_commits?` + expect(project2).to receive(:present?).and_return(false) + + range.valid_commits?(project2) + end + + context 'with a project' do + let(:project) { double('project', repository: double('repository')) } + + context 'with a valid repo' do + before do + expect(project).to receive(:valid_repo?).and_return(true) + range.project = project + end + + it 'is false when `sha_from` is invalid' do + expect(project.repository).to receive(:commit).with(sha_from).and_return(false) + expect(project.repository).not_to receive(:commit).with(sha_to) + expect(range).not_to be_valid_commits + end + + it 'is false when `sha_to` is invalid' do + expect(project.repository).to receive(:commit).with(sha_from).and_return(true) + expect(project.repository).to receive(:commit).with(sha_to).and_return(false) + expect(range).not_to be_valid_commits + end + + it 'is true when both `sha_from` and `sha_to` are valid' do + expect(project.repository).to receive(:commit).with(sha_from).and_return(true) + expect(project.repository).to receive(:commit).with(sha_to).and_return(true) + expect(range).to be_valid_commits + end + end + + context 'without a valid repo' do + before do + expect(project).to receive(:valid_repo?).and_return(false) + range.project = project + end + + it 'returns false' do + expect(range).not_to be_valid_commits + end + end + end + end +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index a6ec44da4be..e303a97e6b5 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -1,27 +1,47 @@ require 'spec_helper' describe Commit do - let(:project) { create :project } - let(:commit) { project.repository.commit } + let(:project) { create(:project) } + let(:commit) { project.commit } + + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Mentionable) } + it { is_expected.to include_module(Participable) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(StaticModel) } + end + + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(commit.to_reference).to eq commit.id + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(commit.to_reference(cross)).to eq "#{project.to_reference}@#{commit.id}" + end + end describe '#title' do it "returns no_commit_message when safe_message is blank" do - commit.stub(:safe_message).and_return('') - commit.title.should == "--no commit message" + allow(commit).to receive(:safe_message).and_return('') + expect(commit.title).to eq("--no commit message") end it "truncates a message without a newline at 80 characters" do message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.' - commit.stub(:safe_message).and_return(message) - commit.title.should == "#{message[0..79]}…" + allow(commit).to receive(:safe_message).and_return(message) + expect(commit.title).to eq("#{message[0..79]}…") end it "truncates a message with a newline before 80 characters at the newline" do message = commit.safe_message.split(" ").first - commit.stub(:safe_message).and_return(message + "\n" + message) - commit.title.should == message + allow(commit).to receive(:safe_message).and_return(message + "\n" + message) + expect(commit.title).to eq(message) end it "does not truncates a message with a newline after 80 but less 100 characters" do @@ -30,25 +50,25 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis Vivamus egestas lacinia lacus, sed rutrum mauris. eos - commit.stub(:safe_message).and_return(message) - commit.title.should == message.split("\n").first + allow(commit).to receive(:safe_message).and_return(message) + expect(commit.title).to eq(message.split("\n").first) end end describe "delegation" do subject { commit } - it { should respond_to(:message) } - it { should respond_to(:authored_date) } - it { should respond_to(:committed_date) } - it { should respond_to(:committer_email) } - it { should respond_to(:author_email) } - it { should respond_to(:parents) } - it { should respond_to(:date) } - it { should respond_to(:diffs) } - it { should respond_to(:tree) } - it { should respond_to(:id) } - it { should respond_to(:to_patch) } + it { is_expected.to respond_to(:message) } + it { is_expected.to respond_to(:authored_date) } + it { is_expected.to respond_to(:committed_date) } + it { is_expected.to respond_to(:committer_email) } + it { is_expected.to respond_to(:author_email) } + it { is_expected.to respond_to(:parents) } + it { is_expected.to respond_to(:date) } + it { is_expected.to respond_to(:diffs) } + it { is_expected.to respond_to(:tree) } + it { is_expected.to respond_to(:id) } + it { is_expected.to respond_to(:to_patch) } end describe '#closes_issues' do @@ -57,26 +77,25 @@ eos let(:other_issue) { create :issue, project: other_project } it 'detects issues that this commit is marked as closing' do - stub_const('Gitlab::ClosingIssueExtractor::ISSUE_CLOSING_REGEX', - /Fixes #\d+/) - commit.stub(safe_message: "Fixes ##{issue.iid}") - commit.closes_issues(project).should == [issue] + allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid}") + expect(commit.closes_issues).to eq([issue]) end it 'does not detect issues from other projects' do ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}" - stub_const('Gitlab::ClosingIssueExtractor::ISSUE_CLOSING_REGEX', - /^([Cc]loses|[Ff]ixes)/) - commit.stub(safe_message: "Fixes #{ext_ref}") - commit.closes_issues(project).should be_empty + allow(commit).to receive(:safe_message).and_return("Fixes #{ext_ref}") + expect(commit.closes_issues).to be_empty end end it_behaves_like 'a mentionable' do - let(:subject) { commit } - let(:mauthor) { create :user, email: commit.author_email } + subject { commit } + + let(:author) { create(:user, email: commit.author_email) } let(:backref_text) { "commit #{subject.id}" } - let(:set_mentionable_text) { ->(txt){ subject.stub(safe_message: txt) } } + let(:set_mentionable_text) do + ->(txt) { allow(subject).to receive(:safe_message).and_return(txt) } + end # Include the subject in the repository stub. let(:extra_commits) { [subject] } diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 9cbc8990676..b6d80451d2e 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -4,63 +4,66 @@ describe Issue, "Issuable" do let(:issue) { create(:issue) } describe "Associations" do - it { should belong_to(:project) } - it { should belong_to(:author) } - it { should belong_to(:assignee) } - it { should have_many(:notes).dependent(:destroy) } + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:author) } + it { is_expected.to belong_to(:assignee) } + it { is_expected.to have_many(:notes).dependent(:destroy) } end describe "Validation" do - before { subject.stub(set_iid: false) } - it { should validate_presence_of(:project) } - it { should validate_presence_of(:iid) } - it { should validate_presence_of(:author) } - it { should validate_presence_of(:title) } - it { should ensure_length_of(:title).is_at_least(0).is_at_most(255) } + before do + allow(subject).to receive(:set_iid).and_return(false) + end + + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:iid) } + it { is_expected.to validate_presence_of(:author) } + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_length_of(:title).is_at_least(0).is_at_most(255) } end describe "Scope" do - it { described_class.should respond_to(:opened) } - it { described_class.should respond_to(:closed) } - it { described_class.should respond_to(:assigned) } + it { expect(described_class).to respond_to(:opened) } + it { expect(described_class).to respond_to(:closed) } + it { expect(described_class).to respond_to(:assigned) } end describe ".search" do let!(:searchable_issue) { create(:issue, title: "Searchable issue") } it "matches by title" do - described_class.search('able').should == [searchable_issue] + expect(described_class.search('able')).to eq([searchable_issue]) end end describe "#today?" do it "returns true when created today" do # Avoid timezone differences and just return exactly what we want - Date.stub(:today).and_return(issue.created_at.to_date) - issue.today?.should be_true + allow(Date).to receive(:today).and_return(issue.created_at.to_date) + expect(issue.today?).to be_truthy end it "returns false when not created today" do - Date.stub(:today).and_return(Date.yesterday) - issue.today?.should be_false + allow(Date).to receive(:today).and_return(Date.yesterday) + expect(issue.today?).to be_falsey end end describe "#new?" do it "returns true when created today and record hasn't been updated" do - issue.stub(:today?).and_return(true) - issue.new?.should be_true + allow(issue).to receive(:today?).and_return(true) + expect(issue.new?).to be_truthy end it "returns false when not created today" do - issue.stub(:today?).and_return(false) - issue.new?.should be_false + allow(issue).to receive(:today?).and_return(false) + expect(issue.new?).to be_falsey end it "returns false when record has been updated" do - issue.stub(:today?).and_return(true) + allow(issue).to receive(:today?).and_return(true) issue.touch - issue.new?.should be_false + expect(issue.new?).to be_falsey end end end diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index ca6f11b2a4d..f7f66987b5f 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -1,14 +1,31 @@ require 'spec_helper' describe Issue, "Mentionable" do - describe :mentioned_users do + describe '#mentioned_users' do let!(:user) { create(:user, username: 'stranger') } let!(:user2) { create(:user, username: 'john') } - let!(:issue) { create(:issue, description: '@stranger mentioned') } + let!(:issue) { create(:issue, description: "#{user.to_reference} mentioned") } subject { issue.mentioned_users } - it { should include(user) } - it { should_not include(user2) } + it { is_expected.to include(user) } + it { is_expected.not_to include(user2) } + end + + describe '#create_cross_references!' do + let(:project) { create(:project) } + let(:author) { double('author') } + let(:commit) { project.commit } + let(:commit2) { project.commit } + + let!(:issue) do + create(:issue, project: project, description: commit.to_reference) + end + + it 'correctly removes already-mentioned Commits' do + expect(SystemNoteService).not_to receive(:cross_reference) + + issue.create_cross_references!(project, author, [commit2]) + end end end diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb index adbbbac875f..95729932459 100644 --- a/spec/models/deploy_key_spec.rb +++ b/spec/models/deploy_key_spec.rb @@ -10,6 +10,7 @@ # title :string(255) # type :string(255) # fingerprint :string(255) +# public :boolean default(FALSE), not null # require 'spec_helper' @@ -19,7 +20,7 @@ describe DeployKey do let(:deploy_key) { create(:deploy_key, projects: [project]) } describe "Associations" do - it { should have_many(:deploy_keys_projects) } - it { should have_many(:projects) } + it { is_expected.to have_many(:deploy_keys_projects) } + it { is_expected.to have_many(:projects) } end end diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb index 3e0e25ee39a..705ef257d86 100644 --- a/spec/models/deploy_keys_project_spec.rb +++ b/spec/models/deploy_keys_project_spec.rb @@ -13,12 +13,56 @@ require 'spec_helper' describe DeployKeysProject do describe "Associations" do - it { should belong_to(:deploy_key) } - it { should belong_to(:project) } + it { is_expected.to belong_to(:deploy_key) } + it { is_expected.to belong_to(:project) } end describe "Validation" do - it { should validate_presence_of(:project_id) } - it { should validate_presence_of(:deploy_key_id) } + it { is_expected.to validate_presence_of(:project_id) } + it { is_expected.to validate_presence_of(:deploy_key_id) } + end + + describe "Destroying" do + let(:project) { create(:project) } + subject { create(:deploy_keys_project, project: project) } + let(:deploy_key) { subject.deploy_key } + + context "when the deploy key is only used by this project" do + context "when the deploy key is public" do + before do + deploy_key.update_attribute(:public, true) + end + + it "doesn't destroy the deploy key" do + subject.destroy + + expect { deploy_key.reload }.not_to raise_error + end + end + + context "when the deploy key is private" do + it "destroys the deploy key" do + subject.destroy + + expect { + deploy_key.reload + }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + + context "when the deploy key is used by more than one project" do + let!(:other_project) { create(:project) } + + before do + other_project.deploy_keys << deploy_key + end + + it "doesn't destroy the deploy key" do + subject.destroy + + expect { deploy_key.reload }.not_to raise_error + end + end end end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 204ae9da704..0f32f162a10 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -18,16 +18,16 @@ require 'spec_helper' describe Event do describe "Associations" do - it { should belong_to(:project) } - it { should belong_to(:target) } + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:target) } end describe "Respond to" do - it { should respond_to(:author_name) } - it { should respond_to(:author_email) } - it { should respond_to(:issue_title) } - it { should respond_to(:merge_request_title) } - it { should respond_to(:commits) } + it { is_expected.to respond_to(:author_name) } + it { is_expected.to respond_to(:author_email) } + it { is_expected.to respond_to(:issue_title) } + it { is_expected.to respond_to(:merge_request_title) } + it { is_expected.to respond_to(:commits) } end describe "Push event" do @@ -58,10 +58,10 @@ describe Event do ) end - it { @event.push?.should be_true } - it { @event.proper?.should be_true } - it { @event.tag?.should be_false } - it { @event.branch_name.should == "master" } - it { @event.author.should == @user } + it { expect(@event.push?).to be_truthy } + it { expect(@event.proper?).to be_truthy } + it { expect(@event.tag?).to be_falsey } + it { expect(@event.branch_name).to eq("master") } + it { expect(@event.author).to eq(@user) } end end diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb new file mode 100644 index 00000000000..7744610db78 --- /dev/null +++ b/spec/models/external_issue_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe ExternalIssue do + let(:project) { double('project', to_reference: 'namespace1/project1') } + let(:issue) { described_class.new('EXT-1234', project) } + + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Referable) } + end + + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(issue.to_reference).to eq issue.id + end + end + + describe '#title' do + it 'returns a title' do + expect(issue.title).to eq "External Issue #{issue}" + end + end +end diff --git a/spec/models/external_wiki_service_spec.rb b/spec/models/external_wiki_service_spec.rb new file mode 100644 index 00000000000..4bd5b0be61c --- /dev/null +++ b/spec/models/external_wiki_service_spec.rb @@ -0,0 +1,59 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe ExternalWikiService do + include ExternalWikiHelper + describe "Associations" do + it { should belong_to :project } + it { should have_one :service_hook } + end + + describe "Validations" do + context "active" do + before do + subject.active = true + end + + it { should validate_presence_of :external_wiki_url } + end + end + + describe 'External wiki' do + let(:project) { create(:project) } + + context 'when it is active' do + before do + properties = { 'external_wiki_url' => 'https://gitlab.com' } + @service = project.create_external_wiki_service(active: true, properties: properties) + end + + after do + @service.destroy! + end + + it 'should replace the wiki url' do + wiki_path = get_project_wiki_path(project) + expect(wiki_path).to match('https://gitlab.com') + end + end + end +end diff --git a/spec/models/flowdock_service_spec.rb b/spec/models/flowdock_service_spec.rb deleted file mode 100644 index 5540f0fa988..00000000000 --- a/spec/models/flowdock_service_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -require 'spec_helper' - -describe FlowdockService do - describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } - end - - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project) } - - before do - @flowdock_service = FlowdockService.new - @flowdock_service.stub( - project_id: project.id, - project: project, - service_hook: true, - token: 'verySecret' - ) - @sample_data = GitPushService.new.sample_data(project, user) - @api_url = 'https://api.flowdock.com/v1/git/verySecret' - WebMock.stub_request(:post, @api_url) - end - - it "should call FlowDock API" do - @flowdock_service.execute(@sample_data) - WebMock.should have_requested(:post, @api_url).with( - body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/ - ).once - end - end -end diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb index 1845c6103f5..d90fbfe1ea5 100644 --- a/spec/models/forked_project_link_spec.rb +++ b/spec/models/forked_project_link_spec.rb @@ -21,11 +21,11 @@ describe ForkedProjectLink, "add link on fork" do end it "project_to should know it is forked" do - @project_to.forked?.should be_true + expect(@project_to.forked?).to be_truthy end it "project should know who it is forked from" do - @project_to.forked_from_project.should == project_from + expect(@project_to.forked_from_project).to eq(project_from) end end @@ -43,25 +43,25 @@ describe :forked_from_project do it "project_to should know it is forked" do - project_to.forked?.should be_true + expect(project_to.forked?).to be_truthy end it "project_from should not be forked" do - project_from.forked?.should be_false + expect(project_from.forked?).to be_falsey end it "project_to.destroy should destroy fork_link" do - forked_project_link.should_receive(:destroy) + expect(forked_project_link).to receive(:destroy) project_to.destroy end end def fork_project(from_project, user) - context = Projects::ForkService.new(from_project, user) - shell = double("gitlab_shell") - shell.stub(fork_repository: true) - context.stub(gitlab_shell: shell) - context.execute -end + shell = double('gitlab_shell', fork_repository: true) + + service = Projects::ForkService.new(from_project, user) + allow(service).to receive(:gitlab_shell).and_return(shell) + service.execute +end diff --git a/spec/models/gemnasium_service_spec.rb b/spec/models/gemnasium_service_spec.rb deleted file mode 100644 index 60ffa6f8b05..00000000000 --- a/spec/models/gemnasium_service_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -require 'spec_helper' - -describe GemnasiumService do - describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } - end - - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project) } - - before do - @gemnasium_service = GemnasiumService.new - @gemnasium_service.stub( - project_id: project.id, - project: project, - service_hook: true, - token: 'verySecret', - api_key: 'GemnasiumUserApiKey' - ) - @sample_data = GitPushService.new.sample_data(project, user) - end - it "should call Gemnasium service" do - Gemnasium::GitlabService.should_receive(:execute).with(an_instance_of(Hash)).once - @gemnasium_service.execute(@sample_data) - end - end -end diff --git a/spec/models/gitlab_ci_service_spec.rb b/spec/models/gitlab_ci_service_spec.rb deleted file mode 100644 index 83277058fbb..00000000000 --- a/spec/models/gitlab_ci_service_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -require 'spec_helper' - -describe GitlabCiService do - describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } - end - - describe "Mass assignment" do - end - - describe 'commits methods' do - before do - @service = GitlabCiService.new - @service.stub( - service_hook: true, - project_url: 'http://ci.gitlab.org/projects/2', - token: 'verySecret' - ) - end - - describe :commit_status_path do - it { @service.commit_status_path("2ab7834c").should == "http://ci.gitlab.org/projects/2/commits/2ab7834c/status.json?token=verySecret"} - end - - describe :build_page do - it { @service.build_page("2ab7834c").should == "http://ci.gitlab.org/projects/2/commits/2ab7834c"} - end - end -end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 1d4ba8a2b85..80638fc8db2 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -18,30 +18,44 @@ require 'spec_helper' describe Group do let!(:group) { create(:group) } - describe "Associations" do - it { should have_many :projects } - it { should have_many :group_members } + describe 'associations' do + it { is_expected.to have_many :projects } + it { is_expected.to have_many :group_members } end - it { should validate_presence_of :name } - it { should validate_uniqueness_of(:name) } - it { should validate_presence_of :path } - it { should validate_uniqueness_of(:path) } - it { should_not validate_presence_of :owner } + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Referable) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of :name } + it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_presence_of :path } + it { is_expected.to validate_uniqueness_of(:path) } + it { is_expected.not_to validate_presence_of :owner } + end + + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(group.to_reference).to eq "@#{group.name}" + end + end describe :users do - it { group.users.should == group.owners } + it { expect(group.users).to eq(group.owners) } end describe :human_name do - it { group.human_name.should == group.name } + it { expect(group.human_name).to eq(group.name) } end describe :add_users do let(:user) { create(:user) } before { group.add_user(user, GroupMember::MASTER) } - it { group.group_members.masters.map(&:user).should include(user) } + it { expect(group.group_members.masters.map(&:user)).to include(user) } end describe :add_users do @@ -49,10 +63,10 @@ describe Group do before { group.add_users([user.id], GroupMember::GUEST) } it "should update the group permission" do - group.group_members.guests.map(&:user).should include(user) + expect(group.group_members.guests.map(&:user)).to include(user) group.add_users([user.id], GroupMember::DEVELOPER) - group.group_members.developers.map(&:user).should include(user) - group.group_members.guests.map(&:user).should_not include(user) + expect(group.group_members.developers.map(&:user)).to include(user) + expect(group.group_members.guests.map(&:user)).not_to include(user) end end @@ -62,12 +76,12 @@ describe Group do it "should be true if avatar is image" do group.update_attribute(:avatar, 'uploads/avatar.png') - group.avatar_type.should be_true + expect(group.avatar_type).to be_truthy end it "should be false if avatar is html page" do group.update_attribute(:avatar, 'uploads/avatar.html') - group.avatar_type.should == ["only images allowed"] + expect(group.avatar_type).to eq(["only images allowed"]) end end end diff --git a/spec/models/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb index 4e0d50d7f3f..dae7e399cfb 100644 --- a/spec/models/project_hook_spec.rb +++ b/spec/models/hooks/project_hook_spec.rb @@ -13,6 +13,7 @@ # 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 # require 'spec_helper' diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb new file mode 100644 index 00000000000..a218b327d76 --- /dev/null +++ b/spec/models/hooks/service_hook_spec.rb @@ -0,0 +1,58 @@ +# == Schema Information +# +# Table name: web_hooks +# +# id :integer not null, primary key +# url :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# type :string(255) 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 +# + +require "spec_helper" + +describe ServiceHook do + describe "Associations" do + it { is_expected.to belong_to :service } + end + + describe "execute" do + before(:each) do + @service_hook = create(:service_hook) + @data = { project_id: 1, data: {}} + + WebMock.stub_request(:post, @service_hook.url) + end + + it "POSTs to the web hook 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'} + ).once + end + + it "POSTs the data as JSON" do + json = @data.to_json + + @service_hook.execute(@data) + expect(WebMock).to have_requested(:post, @service_hook.url).with( + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook'} + ).once + end + + it "catches exceptions" do + expect(WebHook).to receive(:post).and_raise("Some HTTP Post error") + + expect { + @service_hook.execute(@data) + }.to raise_error(RuntimeError) + end + end +end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb new file mode 100644 index 00000000000..edb21fc2e47 --- /dev/null +++ b/spec/models/hooks/system_hook_spec.rb @@ -0,0 +1,123 @@ +# == Schema Information +# +# Table name: web_hooks +# +# id :integer not null, primary key +# url :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# type :string(255) 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 +# + +require "spec_helper" + +describe SystemHook do + describe "execute" do + before(:each) do + @system_hook = create(:system_hook) + WebMock.stub_request(:post, @system_hook.url) + end + + it "project_create hook" do + Projects::CreateService.new(create(: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'} + ).once + end + + it "project_destroy hook" do + user = create(:user) + project = create(:empty_project, namespace: user.namespace) + Projects::DestroyService.new(project, user, {}).execute + expect(WebMock).to have_requested(:post, @system_hook.url).with( + body: /project_destroy/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} + ).once + end + + it "user_create hook" do + create(:user) + expect(WebMock).to have_requested(:post, @system_hook.url).with( + body: /user_create/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} + ).once + end + + it "user_destroy hook" do + user = create(:user) + user.destroy + expect(WebMock).to have_requested(:post, @system_hook.url).with( + body: /user_destroy/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} + ).once + end + + it "project_create hook" do + user = create(:user) + project = create(:project) + project.team << [user, :master] + 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'} + ).once + end + + it "project_destroy hook" do + user = create(:user) + project = create(:project) + project.team << [user, :master] + project.project_members.destroy_all + 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'} + ).once + end + + it 'group create hook' do + create(:group) + expect(WebMock).to have_requested(:post, @system_hook.url).with( + body: /group_create/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} + ).once + end + + it 'group destroy hook' do + group = create(:group) + group.destroy + expect(WebMock).to have_requested(:post, @system_hook.url).with( + body: /group_destroy/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} + ).once + end + + it 'group member create hook' do + group = create(:group) + user = create(:user) + group.add_user(user, Gitlab::Access::MASTER) + 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'} + ).once + end + + it 'group member destroy hook' do + group = create(:group) + user = create(:user) + group.add_user(user, Gitlab::Access::MASTER) + group.group_members.destroy_all + 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'} + ).once + end + + end +end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb new file mode 100644 index 00000000000..b51e6b4e619 --- /dev/null +++ b/spec/models/hooks/web_hook_spec.rb @@ -0,0 +1,79 @@ +# == Schema Information +# +# Table name: web_hooks +# +# id :integer not null, primary key +# url :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# type :string(255) 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 +# + +require 'spec_helper' + +describe ProjectHook do + describe "Associations" do + it { is_expected.to belong_to :project } + end + + describe "Mass assignment" do + end + + describe "Validations" do + it { is_expected.to validate_presence_of(:url) } + + context "url format" do + it { is_expected.to allow_value("http://example.com").for(:url) } + it { is_expected.to allow_value("https://excample.com").for(:url) } + it { is_expected.to allow_value("http://test.com/api").for(:url) } + it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) } + it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) } + + it { is_expected.not_to allow_value("example.com").for(:url) } + it { is_expected.not_to allow_value("ftp://example.com").for(:url) } + it { is_expected.not_to allow_value("herp-and-derp").for(:url) } + end + end + + describe "execute" do + before(:each) do + @project_hook = create(:project_hook) + @project = create(:project) + @project.hooks << [@project_hook] + @data = { before: 'oldrev', after: 'newrev', ref: 'ref'} + + WebMock.stub_request(:post, @project_hook.url) + end + + it "POSTs to the web hook URL" do + @project_hook.execute(@data, 'push_hooks') + expect(WebMock).to have_requested(:post, @project_hook.url).with( + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook'} + ).once + end + + it "POSTs the data as JSON" do + json = @data.to_json + + @project_hook.execute(@data, 'push_hooks') + expect(WebMock).to have_requested(:post, @project_hook.url).with( + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook'} + ).once + end + + it "catches exceptions" do + expect(WebHook).to receive(:post).and_raise("Some HTTP Post error") + + expect { + @project_hook.execute(@data, 'push_hooks') + }.to raise_error(RuntimeError) + end + end +end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 6b6efe832e5..9bac451c28c 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -21,43 +21,56 @@ require 'spec_helper' describe Issue do describe "Associations" do - it { should belong_to(:milestone) } - end - - describe "Mass assignment" do + it { is_expected.to belong_to(:milestone) } end describe 'modules' do - it { should include_module(Issuable) } + subject { described_class } + + it { is_expected.to include_module(InternalId) } + it { is_expected.to include_module(Issuable) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } + it { is_expected.to include_module(Taskable) } end subject { create(:issue) } + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(subject.to_reference).to eq "##{subject.iid}" + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(subject.to_reference(cross)). + to eq "#{subject.project.to_reference}##{subject.iid}" + end + end + describe '#is_being_reassigned?' do it 'returns true if the issue assignee has changed' do subject.assignee = create(:user) - subject.is_being_reassigned?.should be_true + expect(subject.is_being_reassigned?).to be_truthy end it 'returns false if the issue assignee has not changed' do - subject.is_being_reassigned?.should be_false + expect(subject.is_being_reassigned?).to be_falsey end end describe '#is_being_reassigned?' do it 'returns issues assigned to user' do - user = create :user + user = create(:user) + create_list(:issue, 2, assignee: user) - 2.times do - issue = create :issue, assignee: user - end - - Issue.open_for(user).count.should eq 2 + expect(Issue.open_for(user).count).to eq 2 end end it_behaves_like 'an editable mentionable' do - let(:subject) { create :issue, project: mproject } - let(:backref_text) { "issue ##{subject.iid}" } + subject { create(:issue, project: project) } + + let(:backref_text) { "issue #{subject.to_reference}" } let(:set_mentionable_text) { ->(txt){ subject.description = txt } } end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 95c0aed0ffe..fbb9e162952 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -10,73 +10,79 @@ # title :string(255) # type :string(255) # fingerprint :string(255) +# public :boolean default(FALSE), not null # require 'spec_helper' describe Key do describe "Associations" do - it { should belong_to(:user) } + it { is_expected.to belong_to(:user) } end describe "Mass assignment" do end describe "Validation" do - it { should validate_presence_of(:title) } - it { should validate_presence_of(:key) } - it { should ensure_length_of(:title).is_within(0..255) } - it { should ensure_length_of(:key).is_within(0..5000) } + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_presence_of(:key) } + it { is_expected.to validate_length_of(:title).is_within(0..255) } + it { is_expected.to validate_length_of(:key).is_within(0..5000) } end describe "Methods" do - it { should respond_to :projects } + it { is_expected.to respond_to :projects } end context "validation of uniqueness" do let(:user) { create(:user) } it "accepts the key once" do - build(:key, user: user).should be_valid + expect(build(:key, user: user)).to be_valid end it "does not accept the exact same key twice" do create(:key, user: user) - build(:key, user: user).should_not be_valid + expect(build(:key, user: user)).not_to be_valid end it "does not accept a duplicate key with a different comment" do create(:key, user: user) duplicate = build(:key, user: user) duplicate.key << ' extra comment' - duplicate.should_not be_valid + expect(duplicate).not_to be_valid end end context "validate it is a fingerprintable key" do it "accepts the fingerprintable key" do - build(:key).should be_valid + expect(build(:key)).to be_valid end - it "rejects the unfingerprintable key (contains space in middle)" do - build(:key_with_a_space_in_the_middle).should_not be_valid + it 'rejects an unfingerprintable key that contains a space' do + key = build(:key) + + # Not always the middle, but close enough + key.key = key.key[0..100] + ' ' + key.key[100..-1] + + expect(key).not_to be_valid end - it "rejects the unfingerprintable key (not a key)" do - build(:invalid_key).should_not be_valid + it 'rejects the unfingerprintable key (not a key)' do + expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid end end context 'callbacks' do it 'should add new key to authorized_file' do @key = build(:personal_key, id: 7) - GitlabShellWorker.should_receive(:perform_async).with(:add_key, @key.shell_id, @key.key) + expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, @key.shell_id, @key.key) @key.save end it 'should remove key from authorized_file' do @key = create(:personal_key) - GitlabShellWorker.should_receive(:perform_async).with(:remove_key, @key.shell_id, @key.key) + expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, @key.shell_id, @key.key) @key.destroy end end diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb index 0db60432ad3..8c240826582 100644 --- a/spec/models/label_link_spec.rb +++ b/spec/models/label_link_spec.rb @@ -14,8 +14,8 @@ require 'spec_helper' describe LabelLink do let(:label) { create(:label_link) } - it { label.should be_valid } + it { expect(label).to be_valid } - it { should belong_to(:label) } - it { should belong_to(:target) } + it { is_expected.to belong_to(:label) } + it { is_expected.to belong_to(:target) } end diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 31634648f04..6518213d71c 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -14,30 +14,63 @@ require 'spec_helper' describe Label do let(:label) { create(:label) } - it { label.should be_valid } - it { should belong_to(:project) } + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:label_links).dependent(:destroy) } + it { is_expected.to have_many(:issues).through(:label_links).source(:target) } + end + + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Referable) } + end + + describe 'validation' do + it { is_expected.to validate_presence_of(:project) } - describe 'Validation' do it 'should validate color code' do - build(:label, color: 'G-ITLAB').should_not be_valid - build(:label, color: 'AABBCC').should_not be_valid - build(:label, color: '#AABBCCEE').should_not be_valid - build(:label, color: '#GGHHII').should_not be_valid - build(:label, color: '#').should_not be_valid - build(:label, color: '').should_not be_valid - - build(:label, color: '#AABBCC').should be_valid + expect(label).not_to allow_value('G-ITLAB').for(:color) + expect(label).not_to allow_value('AABBCC').for(:color) + expect(label).not_to allow_value('#AABBCCEE').for(:color) + expect(label).not_to allow_value('GGHHII').for(:color) + expect(label).not_to allow_value('#').for(:color) + expect(label).not_to allow_value('').for(:color) + + expect(label).to allow_value('#AABBCC').for(:color) + expect(label).to allow_value('#abcdef').for(:color) end it 'should validate title' do - build(:label, title: 'G,ITLAB').should_not be_valid - build(:label, title: 'G?ITLAB').should_not be_valid - build(:label, title: 'G&ITLAB').should_not be_valid - build(:label, title: '').should_not be_valid + expect(label).not_to allow_value('G,ITLAB').for(:title) + expect(label).not_to allow_value('G?ITLAB').for(:title) + expect(label).not_to allow_value('G&ITLAB').for(:title) + expect(label).not_to allow_value('').for(:title) + + expect(label).to allow_value('GITLAB').for(:title) + expect(label).to allow_value('gitlab').for(:title) + expect(label).to allow_value("customer's request").for(:title) + end + end + + describe '#to_reference' do + context 'using id' do + it 'returns a String reference to the object' do + expect(label.to_reference).to eq "~#{label.id}" + expect(label.to_reference(double('project'))).to eq "~#{label.id}" + end + end + + context 'using name' do + it 'returns a String reference to the object' do + expect(label.to_reference(:name)).to eq %(~"#{label.name}") + end - build(:label, title: 'GITLAB').should be_valid - build(:label, title: 'gitlab').should be_valid + it 'uses id when name contains double quote' do + label = create(:label, name: %q{"irony"}) + expect(label.to_reference(:name)).to eq "~#{label.id}" + end end end end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb new file mode 100644 index 00000000000..57f840c1e91 --- /dev/null +++ b/spec/models/member_spec.rb @@ -0,0 +1,167 @@ +# == 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 do + describe "Associations" do + it { is_expected.to belong_to(:user) } + end + + describe "Validation" do + subject { Member.new(access_level: Member::GUEST) } + + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:source) } + it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) } + + context "when an invite email is provided" do + let(:member) { build(:project_member, invite_email: "user@example.com", user: nil) } + + it "doesn't require a user" do + expect(member).to be_valid + end + + it "requires a valid invite email" do + member.invite_email = "nope" + + expect(member).not_to be_valid + end + + it "requires a unique invite email scoped to this source" do + create(:project_member, source: member.source, invite_email: member.invite_email) + + expect(member).not_to be_valid + end + + it "is valid otherwise" do + expect(member).to be_valid + end + end + + context "when an invite email is not provided" do + let(:member) { build(:project_member) } + + it "requires a user" do + member.user = nil + + expect(member).not_to be_valid + end + + it "is valid otherwise" do + expect(member).to be_valid + end + end + end + + describe "Delegate methods" do + it { is_expected.to respond_to(:user_name) } + it { is_expected.to respond_to(:user_email) } + end + + describe ".add_user" do + let!(:user) { create(:user) } + let(:project) { create(:project) } + + context "when called with a user id" do + it "adds the user as a member" do + Member.add_user(project.project_members, user.id, ProjectMember::MASTER) + + expect(project.users).to include(user) + end + end + + context "when called with a user object" do + it "adds the user as a member" do + Member.add_user(project.project_members, user, ProjectMember::MASTER) + + expect(project.users).to include(user) + end + end + + context "when called with a known user email" do + it "adds the user as a member" do + Member.add_user(project.project_members, user.email, ProjectMember::MASTER) + + expect(project.users).to include(user) + end + end + + context "when called with an unknown user email" do + it "adds a member invite" do + Member.add_user(project.project_members, "user@example.com", ProjectMember::MASTER) + + expect(project.project_members.invite.pluck(:invite_email)).to include("user@example.com") + end + end + end + + describe "#accept_invite!" do + let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) } + let(:user) { create(:user) } + + it "resets the invite token" do + member.accept_invite!(user) + + expect(member.invite_token).to be_nil + end + + it "sets the invite accepted timestamp" do + member.accept_invite!(user) + + expect(member.invite_accepted_at).not_to be_nil + end + + it "sets the user" do + member.accept_invite!(user) + + expect(member.user).to eq(user) + end + + it "calls #after_accept_invite" do + expect(member).to receive(:after_accept_invite) + + member.accept_invite!(user) + end + end + + describe "#decline_invite!" do + let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) } + + it "destroys the member" do + member.decline_invite! + + expect(member).to be_destroyed + end + + it "calls #after_decline_invite" do + expect(member).to receive(:after_decline_invite) + + member.decline_invite! + end + end + + describe "#generate_invite_token" do + let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) } + + it "sets the invite token" do + expect { member.generate_invite_token }.to change { member.invite_token} + end + end +end diff --git a/spec/models/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 38657de6793..652026729bb 100644 --- a/spec/models/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -6,11 +6,15 @@ # access_level :integer not null # source_id :integer not null # source_type :string(255) not null -# user_id :integer 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' @@ -20,26 +24,30 @@ describe GroupMember do describe "#after_create" do it "should send email to user" do membership = build(:group_member) - membership.stub(notification_service: double('NotificationService').as_null_object) - membership.should_receive(:notification_service) + + allow(membership).to receive(:notification_service). + and_return(double('NotificationService').as_null_object) + expect(membership).to receive(:notification_service) + membership.save end end describe "#after_update" do before do - @membership = create :group_member - @membership.stub(notification_service: double('NotificationService').as_null_object) + @group_member = create :group_member + allow(@group_member).to receive(:notification_service). + and_return(double('NotificationService').as_null_object) end it "should send email to user" do - @membership.should_receive(:notification_service) - @membership.update_attribute(:access_level, GroupMember::MASTER) + expect(@group_member).to receive(:notification_service) + @group_member.update_attribute(:access_level, GroupMember::MASTER) end it "does not send an email when the access level has not changed" do - @membership.should_not_receive(:notification_service) - @membership.update_attribute(:access_level, GroupMember::OWNER) + expect(@group_member).not_to receive(:notification_service) + @group_member.update_attribute(:access_level, GroupMember::OWNER) end end end diff --git a/spec/models/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 9b5f89b6d7d..5c72cfe1d6a 100644 --- a/spec/models/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -6,11 +6,15 @@ # access_level :integer not null # source_id :integer not null # source_type :string(255) not null -# user_id :integer 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' @@ -33,19 +37,19 @@ describe ProjectMember do @status = @project_2.team.import(@project_1) end - it { @status.should be_true } + it { expect(@status).to be_truthy } describe 'project 2 should get user 1 as developer. user_2 should not be changed' do - it { @project_2.users.should include(@user_1) } - it { @project_2.users.should include(@user_2) } + it { expect(@project_2.users).to include(@user_1) } + it { expect(@project_2.users).to include(@user_2) } - it { @abilities.allowed?(@user_1, :write_project, @project_2).should be_true } - it { @abilities.allowed?(@user_2, :read_project, @project_2).should be_true } + it { expect(@abilities.allowed?(@user_1, :write_project, @project_2)).to be_truthy } + it { expect(@abilities.allowed?(@user_2, :read_project, @project_2)).to be_truthy } end describe 'project 1 should not be changed' do - it { @project_1.users.should include(@user_1) } - it { @project_1.users.should_not include(@user_2) } + it { expect(@project_1.users).to include(@user_1) } + it { expect(@project_1.users).not_to include(@user_2) } end end @@ -64,12 +68,12 @@ describe ProjectMember do ) end - it { @project_1.users.should include(@user_1) } - it { @project_1.users.should include(@user_2) } + it { expect(@project_1.users).to include(@user_1) } + it { expect(@project_1.users).to include(@user_2) } - it { @project_2.users.should include(@user_1) } - it { @project_2.users.should include(@user_2) } + it { expect(@project_2.users).to include(@user_1) } + it { expect(@project_2.users).to include(@user_2) } end describe :truncate_teams do @@ -86,7 +90,7 @@ describe ProjectMember do ProjectMember.truncate_teams([@project_1.id, @project_2.id]) end - it { @project_1.users.should be_empty } - it { @project_2.users.should be_empty } + it { expect(@project_1.users).to be_empty } + it { expect(@project_2.users).to be_empty } end end diff --git a/spec/models/members_spec.rb b/spec/models/members_spec.rb deleted file mode 100644 index 6866c4794c2..00000000000 --- a/spec/models/members_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe Member do - describe "Associations" do - it { should belong_to(:user) } - end - - describe "Validation" do - subject { Member.new(access_level: Member::GUEST) } - - it { should validate_presence_of(:user) } - it { should validate_presence_of(:source) } - it { should ensure_inclusion_of(:access_level).in_array(Gitlab::Access.values) } - end - - describe "Delegate methods" do - it { should respond_to(:user_name) } - it { should respond_to(:user_email) } - end -end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 7b0d261d72f..76f6d8c54c4 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -18,53 +18,75 @@ # iid :integer # description :text # position :integer default(0) +# locked_at :datetime # require 'spec_helper' describe MergeRequest do - describe "Validation" do - it { should validate_presence_of(:target_branch) } - it { should validate_presence_of(:source_branch) } + subject { create(:merge_request) } + + describe 'associations' do + it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') } + it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') } + + it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) } end - describe "Mass assignment" do + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(InternalId) } + it { is_expected.to include_module(Issuable) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } + it { is_expected.to include_module(Taskable) } end - describe "Respond to" do - it { should respond_to(:unchecked?) } - it { should respond_to(:can_be_merged?) } - it { should respond_to(:cannot_be_merged?) } + describe 'validation' do + it { is_expected.to validate_presence_of(:target_branch) } + it { is_expected.to validate_presence_of(:source_branch) } end - describe 'modules' do - it { should include_module(Issuable) } + describe 'respond to' do + it { is_expected.to respond_to(:unchecked?) } + it { is_expected.to respond_to(:can_be_merged?) } + it { is_expected.to respond_to(:cannot_be_merged?) } + end + + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(subject.to_reference).to eq "!#{subject.iid}" + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(subject.to_reference(cross)).to eq "#{subject.source_project.to_reference}!#{subject.iid}" + end end describe "#mr_and_commit_notes" do let!(:merge_request) { create(:merge_request) } before do - merge_request.stub(:commits) { [merge_request.source_project.repository.commit] } + 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, noteable: merge_request, project: merge_request.project) end it "should include notes for commits" do - merge_request.commits.should_not be_empty - merge_request.mr_and_commit_notes.count.should == 2 + expect(merge_request.commits).not_to be_empty + expect(merge_request.mr_and_commit_notes.count).to eq(2) end end - subject { create(:merge_request) } - describe '#is_being_reassigned?' do it 'returns true if the merge_request assignee has changed' do subject.assignee = create(:user) - subject.is_being_reassigned?.should be_true + expect(subject.is_being_reassigned?).to be_truthy end it 'returns false if the merge request assignee has not changed' do - subject.is_being_reassigned?.should be_false + expect(subject.is_being_reassigned?).to be_falsey end end @@ -73,11 +95,11 @@ describe MergeRequest do subject.source_project = create(:project, namespace: create(:group)) subject.target_project = create(:project, namespace: create(:group)) - subject.for_fork?.should be_true + expect(subject.for_fork?).to be_truthy end it 'returns false if is not for a fork' do - subject.for_fork?.should be_false + expect(subject.for_fork?).to be_falsey end end @@ -89,38 +111,67 @@ describe MergeRequest do let(:commit2) { double('commit2', closes_issues: [issue1]) } before do - subject.stub(commits: [commit0, commit1, commit2]) + allow(subject).to receive(:commits).and_return([commit0, commit1, commit2]) end it 'accesses the set of issues that will be closed on acceptance' do - subject.project.stub(default_branch: subject.target_branch) + allow(subject.project).to receive(:default_branch). + and_return(subject.target_branch) - subject.closes_issues.should == [issue0, issue1].sort_by(&:id) + expect(subject.closes_issues).to eq([issue0, issue1].sort_by(&:id)) end it 'only lists issues as to be closed if it targets the default branch' do - subject.project.stub(default_branch: 'master') + allow(subject.project).to receive(:default_branch).and_return('master') subject.target_branch = 'something-else' - subject.closes_issues.should be_empty + expect(subject.closes_issues).to be_empty end it 'detects issues mentioned in the description' do issue2 = create(:issue, project: subject.project) - subject.description = "Closes ##{issue2.iid}" - subject.project.stub(default_branch: subject.target_branch) + subject.description = "Closes #{issue2.to_reference}" + allow(subject.project).to receive(:default_branch). + and_return(subject.target_branch) - subject.closes_issues.should include(issue2) + expect(subject.closes_issues).to include(issue2) + end + end + + describe "#work_in_progress?" do + it "detects the 'WIP ' prefix" do + subject.title = "WIP #{subject.title}" + expect(subject).to be_work_in_progress + end + + it "detects the 'WIP: ' prefix" do + subject.title = "WIP: #{subject.title}" + expect(subject).to be_work_in_progress + end + + it "detects the '[WIP] ' prefix" do + subject.title = "[WIP] #{subject.title}" + expect(subject).to be_work_in_progress + end + + it "doesn't detect WIP for words starting with WIP" do + subject.title = "Wipwap #{subject.title}" + expect(subject).not_to be_work_in_progress + end + + it "doesn't detect WIP by default" do + expect(subject).not_to be_work_in_progress end end it_behaves_like 'an editable mentionable' do - let(:subject) { create :merge_request, source_project: mproject, target_project: mproject } - let(:backref_text) { "merge request !#{subject.iid}" } - let(:set_mentionable_text) { ->(txt){ subject.title = txt } } + subject { create(:merge_request, source_project: project) } + + let(:backref_text) { "merge request #{subject.to_reference}" } + let(:set_mentionable_text) { ->(txt){ subject.description = txt } } end it_behaves_like 'a Taskable' do - let(:subject) { create :merge_request, :simple } + subject { create :merge_request, :simple } end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index a3071c3251a..36352e1ecce 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -17,17 +17,17 @@ require 'spec_helper' describe Milestone do describe "Associations" do - it { should belong_to(:project) } - it { should have_many(:issues) } - end - - describe "Mass assignment" do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:issues) } end describe "Validation" do - before { subject.stub(set_iid: false) } - it { should validate_presence_of(:title) } - it { should validate_presence_of(:project) } + before do + allow(subject).to receive(:set_iid).and_return(false) + end + + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_presence_of(:project) } end let(:milestone) { create(:milestone) } @@ -36,60 +36,60 @@ describe Milestone do describe "#percent_complete" do it "should not count open issues" do milestone.issues << issue - milestone.percent_complete.should == 0 + expect(milestone.percent_complete).to eq(0) end it "should count closed issues" do issue.close milestone.issues << issue - milestone.percent_complete.should == 100 + expect(milestone.percent_complete).to eq(100) end it "should recover from dividing by zero" do - milestone.issues.should_receive(:count).and_return(0) - milestone.percent_complete.should == 100 + expect(milestone.issues).to receive(:count).and_return(0) + expect(milestone.percent_complete).to eq(0) end end describe "#expires_at" do it "should be nil when due_date is unset" do milestone.update_attributes(due_date: nil) - milestone.expires_at.should be_nil + expect(milestone.expires_at).to be_nil end it "should not be nil when due_date is set" do milestone.update_attributes(due_date: Date.tomorrow) - milestone.expires_at.should be_present + expect(milestone.expires_at).to be_present end end describe :expired? do context "expired" do before do - milestone.stub(due_date: Date.today.prev_year) + allow(milestone).to receive(:due_date).and_return(Date.today.prev_year) end - it { milestone.expired?.should be_true } + it { expect(milestone.expired?).to be_truthy } end context "not expired" do before do - milestone.stub(due_date: Date.today.next_year) + allow(milestone).to receive(:due_date).and_return(Date.today.next_year) end - it { milestone.expired?.should be_false } + it { expect(milestone.expired?).to be_falsey } end end describe :percent_complete do before do - milestone.stub( + allow(milestone).to receive_messages( closed_items_count: 3, total_items_count: 4 ) end - it { milestone.percent_complete.should == 75 } + it { expect(milestone.percent_complete).to eq(75) } end describe :items_count do @@ -99,14 +99,14 @@ describe Milestone do milestone.merge_requests << create(:merge_request) end - it { milestone.closed_items_count.should == 1 } - it { milestone.open_items_count.should == 2 } - it { milestone.total_items_count.should == 3 } - it { milestone.is_empty?.should be_false } + it { expect(milestone.closed_items_count).to eq(1) } + it { expect(milestone.open_items_count).to eq(2) } + it { expect(milestone.total_items_count).to eq(3) } + it { expect(milestone.is_empty?).to be_falsey } end describe :can_be_closed? do - it { milestone.can_be_closed?.should be_true } + it { expect(milestone.can_be_closed?).to be_truthy } end describe :is_empty? do @@ -116,7 +116,7 @@ describe Milestone do end it 'Should return total count of issues and merge requests assigned to milestone' do - milestone.total_items_count.should eq 2 + expect(milestone.total_items_count).to eq 2 end end @@ -129,14 +129,14 @@ describe Milestone do end it 'should be true if milestone active and all nested issues closed' do - milestone.can_be_closed?.should be_true + expect(milestone.can_be_closed?).to be_truthy end it 'should be false if milestone active and not all nested issues closed' do issue.milestone = milestone issue.save - milestone.can_be_closed?.should be_false + expect(milestone.can_be_closed?).to be_falsey end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 3562ebed1ff..1d72a9503ae 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -18,29 +18,27 @@ require 'spec_helper' describe Namespace do let!(:namespace) { create(:namespace) } - it { should have_many :projects } - it { should validate_presence_of :name } - it { should validate_uniqueness_of(:name) } - it { should validate_presence_of :path } - it { should validate_uniqueness_of(:path) } - it { should validate_presence_of :owner } + it { is_expected.to have_many :projects } + it { is_expected.to validate_presence_of :name } + it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_presence_of :path } + it { is_expected.to validate_uniqueness_of(:path) } + it { is_expected.to validate_presence_of :owner } describe "Mass assignment" do end describe "Respond to" do - it { should respond_to(:human_name) } - it { should respond_to(:to_param) } + it { is_expected.to respond_to(:human_name) } + it { is_expected.to respond_to(:to_param) } end - it { Namespace.global_id.should == 'GLN' } - describe :to_param do - it { namespace.to_param.should == namespace.path } + it { expect(namespace.to_param).to eq(namespace.path) } end describe :human_name do - it { namespace.human_name.should == namespace.owner_name } + it { expect(namespace.human_name).to eq(namespace.owner_name) } end describe :search do @@ -48,14 +46,14 @@ describe Namespace do @namespace = create :namespace end - it { Namespace.search(@namespace.path).should == [@namespace] } - it { Namespace.search('unknown').should == [] } + it { expect(Namespace.search(@namespace.path)).to eq([@namespace]) } + it { expect(Namespace.search('unknown')).to eq([]) } end describe :move_dir do before do @namespace = create :namespace - @namespace.stub(path_changed?: true) + allow(@namespace).to receive(:path_changed?).and_return(true) end it "should raise error when directory exists" do @@ -64,15 +62,35 @@ describe Namespace do it "should move dir if path changed" do new_path = @namespace.path + "_new" - @namespace.stub(path_was: @namespace.path) - @namespace.stub(path: new_path) - @namespace.move_dir.should be_true + allow(@namespace).to receive(:path_was).and_return(@namespace.path) + allow(@namespace).to receive(:path).and_return(new_path) + expect(@namespace.move_dir).to be_truthy end end describe :rm_dir do it "should remove dir" do - namespace.rm_dir.should be_true + expect(namespace.rm_dir).to be_truthy + end + end + + describe :find_by_path_or_name do + before do + @namespace = create(:namespace, name: 'WoW', path: 'woW') + end + + it { expect(Namespace.find_by_path_or_name('wow')).to eq(@namespace) } + it { expect(Namespace.find_by_path_or_name('WOW')).to eq(@namespace) } + it { expect(Namespace.find_by_path_or_name('unknown')).to eq(nil) } + end + + describe ".clean_path" do + + let!(:user) { create(:user, username: "johngitlab-etc") } + let!(:namespace) { create(:namespace, path: "JohnGitLab-etc1") } + + it "cleans the path and makes sure it's available" do + expect(Namespace.clean_path("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2") end end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 6ab7162c15c..9037992bb08 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -20,89 +20,105 @@ require 'spec_helper' describe Note do - describe "Associations" do - it { should belong_to(:project) } - it { should belong_to(:noteable) } - it { should belong_to(:author).class_name('User') } + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:noteable).touch(true) } + it { is_expected.to belong_to(:author).class_name('User') } end - describe "Mass assignment" do + describe 'validation' do + it { is_expected.to validate_presence_of(:note) } + it { is_expected.to validate_presence_of(:project) } end - describe "Validation" do - it { should validate_presence_of(:note) } - it { should validate_presence_of(:project) } - end + describe '#votable?' do + it 'is true for issue notes' do + note = build(:note_on_issue) + expect(note).to be_votable + end - describe "Voting score" do - let(:project) { create(:project) } + it 'is true for merge request notes' do + note = build(:note_on_merge_request) + expect(note).to be_votable + end - it "recognizes a neutral note" do - note = create(:votable_note, note: "This is not a +1 note") - note.should_not be_upvote - note.should_not be_downvote + it 'is false for merge request diff notes' do + note = build(:note_on_merge_request_diff) + expect(note).not_to be_votable end - it "recognizes a neutral emoji note" do + it 'is false for commit notes' do + note = build(:note_on_commit) + expect(note).not_to be_votable + end + + it 'is false for commit diff notes' do + note = build(:note_on_commit_diff) + expect(note).not_to be_votable + end + end + + describe 'voting score' do + it 'recognizes a neutral note' do + note = build(:votable_note, note: 'This is not a +1 note') + expect(note).not_to be_upvote + expect(note).not_to be_downvote + end + + it 'recognizes a neutral emoji note' do note = build(:votable_note, note: "I would :+1: this, but I don't want to") - note.should_not be_upvote - note.should_not be_downvote + expect(note).not_to be_upvote + expect(note).not_to be_downvote end - it "recognizes a +1 note" do - note = create(:votable_note, note: "+1 for this") - note.should be_upvote + it 'recognizes a +1 note' do + note = build(:votable_note, note: '+1 for this') + expect(note).to be_upvote end - it "recognizes a +1 emoji as a vote" do - note = build(:votable_note, note: ":+1: for this") - note.should be_upvote + it 'recognizes a +1 emoji as a vote' do + note = build(:votable_note, note: ':+1: for this') + expect(note).to be_upvote end - it "recognizes a thumbsup emoji as a vote" do - note = build(:votable_note, note: ":thumbsup: for this") - note.should be_upvote + it 'recognizes a thumbsup emoji as a vote' do + note = build(:votable_note, note: ':thumbsup: for this') + expect(note).to be_upvote end - it "recognizes a -1 note" do - note = create(:votable_note, note: "-1 for this") - note.should be_downvote + it 'recognizes a -1 note' do + note = build(:votable_note, note: '-1 for this') + expect(note).to be_downvote end - it "recognizes a -1 emoji as a vote" do - note = build(:votable_note, note: ":-1: for this") - note.should be_downvote + it 'recognizes a -1 emoji as a vote' do + note = build(:votable_note, note: ':-1: for this') + expect(note).to be_downvote end - it "recognizes a thumbsdown emoji as a vote" do - note = build(:votable_note, note: ":thumbsdown: for this") - note.should be_downvote + it 'recognizes a thumbsdown emoji as a vote' do + note = build(:votable_note, note: ':thumbsdown: for this') + expect(note).to be_downvote end end - let(:project) { create(:project) } - describe "Commit notes" do let!(:note) { create(:note_on_commit, note: "+1 from me") } let!(:commit) { note.noteable } it "should be accessible through #noteable" do - note.commit_id.should == commit.id - note.noteable.should be_a(Commit) - note.noteable.should == commit + expect(note.commit_id).to eq(commit.id) + expect(note.noteable).to be_a(Commit) + expect(note.noteable).to eq(commit) end it "should save a valid note" do - note.commit_id.should == commit.id + expect(note.commit_id).to eq(commit.id) note.noteable == commit end it "should be recognized by #for_commit?" do - note.should be_for_commit - end - - it "should not be votable" do - note.should_not be_votable + expect(note).to be_for_commit end end @@ -111,230 +127,24 @@ describe Note do let!(:commit) { note.noteable } it "should save a valid note" do - note.commit_id.should == commit.id - note.noteable.id.should == commit.id + 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 - note.should be_for_diff_line + expect(note).to be_for_diff_line end it "should be recognized by #for_commit_diff_line?" do - note.should be_for_commit_diff_line - end - - it "should not be votable" do - note.should_not be_votable - end - end - - describe "Issue notes" do - let!(:note) { create(:note_on_issue, note: "+1 from me") } - - it "should not be votable" do - note.should be_votable - end - end - - describe "Merge request notes" do - let!(:note) { create(:note_on_merge_request, note: "+1 from me") } - - it "should be votable" do - note.should be_votable + expect(note).to be_for_commit_diff_line end - end - - describe "Merge request diff line notes" do - let!(:note) { create(:note_on_merge_request_diff, note: "+1 from me") } it "should not be votable" do - note.should_not be_votable + expect(note).not_to be_votable end end - describe '#create_status_change_note' do - let(:project) { create(:project) } - let(:thing) { create(:issue, project: project) } - let(:author) { create(:user) } - let(:status) { 'new_status' } - - subject { Note.create_status_change_note(thing, project, author, status, nil) } - - it 'creates and saves a Note' do - should be_a Note - subject.id.should_not be_nil - end - - its(:noteable) { should == thing } - its(:project) { should == thing.project } - its(:author) { should == author } - its(:note) { should =~ /Status changed to #{status}/ } - - it 'appends a back-reference if a closing mentionable is supplied' do - commit = double('commit', gfm_reference: 'commit 123456') - n = Note.create_status_change_note(thing, project, author, status, commit) - - n.note.should =~ /Status changed to #{status} by commit 123456/ - end - end - - describe '#create_assignee_change_note' do - let(:project) { create(:project) } - let(:thing) { create(:issue, project: project) } - let(:author) { create(:user) } - let(:assignee) { create(:user) } - - subject { Note.create_assignee_change_note(thing, project, author, assignee) } - - context 'creates and saves a Note' do - it { should be_a Note } - its(:id) { should_not be_nil } - end - - its(:noteable) { should == thing } - its(:project) { should == thing.project } - its(:author) { should == author } - its(:note) { should =~ /Reassigned to @#{assignee.username}/ } - - context 'assignee is removed' do - let(:assignee) { nil } - - its(:note) { should =~ /Assignee removed/ } - end - end - - describe '#create_cross_reference_note' do - let(:project) { create(:project) } - let(:author) { create(:user) } - let(:issue) { create(:issue, project: project) } - let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) } - let(:commit) { project.repository.commit } - - # Test all of {issue, merge request, commit} in both the referenced and referencing - # roles, to ensure that the correct information can be inferred from any argument. - - context 'issue from a merge request' do - subject { Note.create_cross_reference_note(issue, mergereq, author, project) } - - it { should be_valid } - its(:noteable) { should == issue } - its(:project) { should == issue.project } - its(:author) { should == author } - its(:note) { should == "_mentioned in merge request !#{mergereq.iid}_" } - end - - context 'issue from a commit' do - subject { Note.create_cross_reference_note(issue, commit, author, project) } - - it { should be_valid } - its(:noteable) { should == issue } - its(:note) { should == "_mentioned in commit #{commit.sha}_" } - end - - context 'merge request from an issue' do - subject { Note.create_cross_reference_note(mergereq, issue, author, project) } - - it { should be_valid } - its(:noteable) { should == mergereq } - its(:project) { should == mergereq.project } - its(:note) { should == "_mentioned in issue ##{issue.iid}_" } - end - - context 'commit from a merge request' do - subject { Note.create_cross_reference_note(commit, mergereq, author, project) } - - it { should be_valid } - its(:noteable) { should == commit } - its(:project) { should == project } - its(:note) { should == "_mentioned in merge request !#{mergereq.iid}_" } - end - - context 'commit contained in a merge request' do - subject { Note.create_cross_reference_note(mergereq.commits.first, mergereq, author, project) } - - it { should be_nil } - end - - context 'commit from issue' do - subject { Note.create_cross_reference_note(commit, issue, author, project) } - - it { should be_valid } - its(:noteable_type) { should == "Commit" } - its(:noteable_id) { should be_nil } - its(:commit_id) { should == commit.id } - its(:note) { should == "_mentioned in issue ##{issue.iid}_" } - end - - context 'commit from commit' do - let(:parent_commit) { commit.parents.first } - subject { Note.create_cross_reference_note(commit, parent_commit, author, project) } - - it { should be_valid } - its(:noteable_type) { should == "Commit" } - its(:noteable_id) { should be_nil } - its(:commit_id) { should == commit.id } - its(:note) { should == "_mentioned in commit #{parent_commit.id}_" } - end - end - - describe '#cross_reference_exists?' do - let(:project) { create :project } - let(:author) { create :user } - let(:issue) { create :issue } - let(:commit0) { project.repository.commit } - let(:commit1) { project.repository.commit('HEAD~2') } - - before do - Note.create_cross_reference_note(issue, commit0, author, project) - end - - it 'detects if a mentionable has already been mentioned' do - Note.cross_reference_exists?(issue, commit0).should be_true - end - - it 'detects if a mentionable has not already been mentioned' do - Note.cross_reference_exists?(issue, commit1).should be_false - end - - context 'commit on commit' do - before do - Note.create_cross_reference_note(commit0, commit1, author, project) - end - - it { Note.cross_reference_exists?(commit0, commit1).should be_true } - it { Note.cross_reference_exists?(commit1, commit0).should be_false } - end - end - - describe '#system?' do - let(:project) { create(:project) } - let(:issue) { create(:issue, project: project) } - let(:other) { create(:issue, project: project) } - let(:author) { create(:user) } - let(:assignee) { create(:user) } - - it 'should recognize user-supplied notes as non-system' do - @note = create(:note_on_issue) - @note.should_not be_system - end - - it 'should identify status-change notes as system notes' do - @note = Note.create_status_change_note(issue, project, author, 'closed', nil) - @note.should be_system - end - - it 'should identify cross-reference notes as system notes' do - @note = Note.create_cross_reference_note(issue, other, author, project) - @note.should be_system - end - - it 'should identify assignee-change notes as system notes' do - @note = Note.create_assignee_change_note(issue, project, author, assignee) - @note.should be_system - end - end - - describe :authorization do + describe 'authorization' do before do @p1 = create(:project) @p2 = create(:project) @@ -345,44 +155,46 @@ describe Note do @abilities << Ability end - describe :read do + describe 'read' do before do @p1.project_members.create(user: @u2, access_level: ProjectMember::GUEST) @p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST) end - it { @abilities.allowed?(@u1, :read_note, @p1).should be_false } - it { @abilities.allowed?(@u2, :read_note, @p1).should be_true } - it { @abilities.allowed?(@u3, :read_note, @p1).should be_false } + it { expect(@abilities.allowed?(@u1, :read_note, @p1)).to be_falsey } + it { expect(@abilities.allowed?(@u2, :read_note, @p1)).to be_truthy } + it { expect(@abilities.allowed?(@u3, :read_note, @p1)).to be_falsey } end - describe :write do + describe 'write' do before do @p1.project_members.create(user: @u2, access_level: ProjectMember::DEVELOPER) @p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER) end - it { @abilities.allowed?(@u1, :write_note, @p1).should be_false } - it { @abilities.allowed?(@u2, :write_note, @p1).should be_true } - it { @abilities.allowed?(@u3, :write_note, @p1).should be_false } + it { expect(@abilities.allowed?(@u1, :write_note, @p1)).to be_falsey } + it { expect(@abilities.allowed?(@u2, :write_note, @p1)).to be_truthy } + it { expect(@abilities.allowed?(@u3, :write_note, @p1)).to be_falsey } end - describe :admin do + describe 'admin' do before do @p1.project_members.create(user: @u1, access_level: ProjectMember::REPORTER) @p1.project_members.create(user: @u2, access_level: ProjectMember::MASTER) @p2.project_members.create(user: @u3, access_level: ProjectMember::MASTER) end - it { @abilities.allowed?(@u1, :admin_note, @p1).should be_false } - it { @abilities.allowed?(@u2, :admin_note, @p1).should be_true } - it { @abilities.allowed?(@u3, :admin_note, @p1).should be_false } + it { expect(@abilities.allowed?(@u1, :admin_note, @p1)).to be_falsey } + it { expect(@abilities.allowed?(@u2, :admin_note, @p1)).to be_truthy } + it { expect(@abilities.allowed?(@u3, :admin_note, @p1)).to be_falsey } end end it_behaves_like 'an editable mentionable' do + subject { create :note, noteable: issue, project: project } + + let(:project) { create(:project) } let(:issue) { create :issue, project: project } - let(:subject) { create :note, noteable: issue, project: project } let(:backref_text) { issue.gfm_reference } let(:set_mentionable_text) { ->(txt) { subject.note = txt } } end diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb index 5c8d1e7438b..1ee19003543 100644 --- a/spec/models/project_security_spec.rb +++ b/spec/models/project_security_spec.rb @@ -23,7 +23,7 @@ describe Project do describe "Non member rules" do it "should deny for non-project users any actions" do admin_actions.each do |action| - @abilities.allowed?(@u1, action, @p1).should be_false + expect(@abilities.allowed?(@u1, action, @p1)).to be_falsey end end end @@ -35,7 +35,7 @@ describe Project do it "should allow for project user any guest actions" do guest_actions.each do |action| - @abilities.allowed?(@u2, action, @p1).should be_true + expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy end end end @@ -47,7 +47,7 @@ describe Project do it "should allow for project user any report actions" do report_actions.each do |action| - @abilities.allowed?(@u2, action, @p1).should be_true + expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy end end end @@ -60,13 +60,13 @@ describe Project do it "should deny for developer master-specific actions" do [dev_actions - report_actions].each do |action| - @abilities.allowed?(@u2, action, @p1).should be_false + expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey end end it "should allow for project user any dev actions" do dev_actions.each do |action| - @abilities.allowed?(@u3, action, @p1).should be_true + expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy end end end @@ -79,13 +79,13 @@ describe Project do it "should deny for developer master-specific actions" do [master_actions - dev_actions].each do |action| - @abilities.allowed?(@u2, action, @p1).should be_false + expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey end end it "should allow for project user any master actions" do master_actions.each do |action| - @abilities.allowed?(@u3, action, @p1).should be_true + expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy end end end @@ -98,13 +98,13 @@ describe Project do it "should deny for masters admin-specific actions" do [admin_actions - master_actions].each do |action| - @abilities.allowed?(@u2, action, @p1).should be_false + expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey end end it "should allow for project owner any admin actions" do admin_actions.each do |action| - @abilities.allowed?(@u4, action, @p1).should be_true + expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy end end end diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb new file mode 100644 index 00000000000..64bb92fba95 --- /dev/null +++ b/spec/models/project_services/asana_service_spec.rb @@ -0,0 +1,66 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe AsanaService, models: true do + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'active' do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of :api_key } + end + end + + describe 'Execute' do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + @asana = AsanaService.new + allow(@asana).to receive_messages( + project: project, + project_id: project.id, + service_hook: true, + api_key: 'verySecret', + restrict_to_branch: 'master' + ) + end + + it 'should call Asana service to created a story' do + expect(Asana::Task).to receive(:find).with('123456').once + + @asana.check_commit('related to #123456', 'pushed') + end + + it 'should call Asana service to created a story and close a task' do + expect(Asana::Task).to receive(:find).with('456789').twice + + @asana.check_commit('fix #456789', 'pushed') + end + end +end diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb new file mode 100644 index 00000000000..17e9361dd5c --- /dev/null +++ b/spec/models/project_services/assembla_service_spec.rb @@ -0,0 +1,54 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe AssemblaService, models: true do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe "Execute" do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + @assembla_service = AssemblaService.new + allow(@assembla_service).to receive_messages( + project_id: project.id, + project: project, + service_hook: true, + token: 'verySecret', + subdomain: 'project_name' + ) + @sample_data = Gitlab::PushDataBuilder.build_sample(project, user) + @api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret' + WebMock.stub_request(:post, @api_url) + end + + it "should call Assembla API" do + @assembla_service.execute(@sample_data) + expect(WebMock).to have_requested(:post, @api_url).with( + body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/ + ).once + end + end +end diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb new file mode 100644 index 00000000000..9445d96c337 --- /dev/null +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -0,0 +1,81 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe BuildkiteService do + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'commits methods' do + before do + @project = Project.new + allow(@project).to receive(:default_branch).and_return('default-brancho') + + @service = BuildkiteService.new + allow(@service).to receive_messages( + project: @project, + service_hook: true, + project_url: 'https://buildkite.com/account-name/example-project', + token: 'secret-sauce-webhook-token:secret-sauce-status-token' + ) + end + + describe :webhook_url do + it 'returns the webhook url' do + expect(@service.webhook_url).to eq( + 'https://webhook.buildkite.com/deliver/secret-sauce-webhook-token' + ) + end + end + + describe :commit_status_path do + it 'returns the correct status page' do + expect(@service.commit_status_path('2ab7834c')).to eq( + 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=2ab7834c' + ) + end + end + + describe :build_page do + it 'returns the correct build page' do + expect(@service.build_page('2ab7834c', nil)).to eq( + 'https://buildkite.com/account-name/example-project/builds?commit=2ab7834c' + ) + end + end + + describe :builds_page do + it 'returns the correct path to the builds page' do + expect(@service.builds_path).to eq( + 'https://buildkite.com/account-name/example-project/builds?branch=default-brancho' + ) + end + end + + describe :status_img_path do + it 'returns the correct path to the status image' do + expect(@service.status_img_path).to eq('https://badge.buildkite.com/secret-sauce-status-token.svg') + end + end + end +end diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb new file mode 100644 index 00000000000..7e5b15cb09e --- /dev/null +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -0,0 +1,53 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe FlowdockService do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe "Execute" do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + @flowdock_service = FlowdockService.new + allow(@flowdock_service).to receive_messages( + project_id: project.id, + project: project, + service_hook: true, + token: 'verySecret' + ) + @sample_data = Gitlab::PushDataBuilder.build_sample(project, user) + @api_url = 'https://api.flowdock.com/v1/git/verySecret' + WebMock.stub_request(:post, @api_url) + end + + it "should call FlowDock API" do + @flowdock_service.execute(@sample_data) + expect(WebMock).to have_requested(:post, @api_url).with( + body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/ + ).once + end + end +end diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb new file mode 100644 index 00000000000..9e156472316 --- /dev/null +++ b/spec/models/project_services/gemnasium_service_spec.rb @@ -0,0 +1,49 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe GemnasiumService do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe "Execute" do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + @gemnasium_service = GemnasiumService.new + allow(@gemnasium_service).to receive_messages( + project_id: project.id, + project: project, + service_hook: true, + token: 'verySecret', + api_key: 'GemnasiumUserApiKey' + ) + @sample_data = Gitlab::PushDataBuilder.build_sample(project, user) + end + it "should call Gemnasium service" do + expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once + @gemnasium_service.execute(@sample_data) + end + end +end diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb new file mode 100644 index 00000000000..fedc37c9b94 --- /dev/null +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -0,0 +1,85 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe GitlabCiService do + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_one(:service_hook) } + end + + describe 'commits methods' do + before do + @service = GitlabCiService.new + allow(@service).to receive_messages( + service_hook: true, + project_url: 'http://ci.gitlab.org/projects/2', + token: 'verySecret' + ) + end + + describe :commit_status_path do + it { expect(@service.commit_status_path("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c/status.json?token=verySecret")} + it { expect(@service.commit_status_path("issue#2", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/issue%232/status.json?token=verySecret")} + end + + describe :build_page do + it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c")} + it { expect(@service.build_page("issue#2", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/issue%232")} + end + + describe "execute" do + let(:user) { create(:user, username: 'username') } + let(:project) { create(:project, name: 'project') } + let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + + it "calls ci_yaml_file" do + service_hook = double + expect(service_hook).to receive(:execute) + expect(@service).to receive(:service_hook).and_return(service_hook) + expect(@service).to receive(:ci_yaml_file).with(push_sample_data[:checkout_sha]) + + @service.execute(push_sample_data) + end + end + end + + describe "Fork registration" do + before do + @old_project = create(:empty_project) + @project = create(:empty_project) + @user = create(:user) + + @service = GitlabCiService.new + allow(@service).to receive_messages( + service_hook: true, + project_url: 'http://ci.gitlab.org/projects/2', + token: 'verySecret', + project: @old_project + ) + end + + it "performs http reuquest to ci" do + stub_request(:post, "http://ci.gitlab.org/api/v1/forks") + @service.fork_registration(@project, @user.private_token) + end + end +end diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb new file mode 100644 index 00000000000..e34ca09bffc --- /dev/null +++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb @@ -0,0 +1,67 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe GitlabIssueTrackerService do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + + describe 'project and issue urls' do + let(:project) { create(:project) } + + context 'with absolute urls' do + before do + GitlabIssueTrackerService.default_url_options[:script_name] = "/gitlab/root" + @service = project.create_gitlab_issue_tracker_service(active: true) + end + + after do + @service.destroy! + end + + it 'should give the correct path' do + expect(@service.project_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues") + expect(@service.new_issue_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/new") + expect(@service.issue_url(432)).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/432") + end + end + + context 'with relative urls' do + before do + GitlabIssueTrackerService.default_url_options[:script_name] = "/gitlab/root" + @service = project.create_gitlab_issue_tracker_service(active: true) + end + + after do + @service.destroy! + end + + it 'should give the correct path' do + expect(@service.project_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues") + expect(@service.new_issue_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new") + expect(@service.issue_path(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432") + end + end + end +end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb new file mode 100644 index 00000000000..8ed03dd1da8 --- /dev/null +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -0,0 +1,258 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe HipchatService do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe "Execute" do + let(:hipchat) { HipchatService.new } + let(:user) { create(:user, username: 'username') } + let(:project) { create(:project, name: 'project') } + let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' } + let(:project_name) { project.name_with_namespace.gsub(/\s/, '') } + let(:token) { 'verySecret' } + let(:server_url) { 'https://hipchat.example.com'} + let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + + before(:each) do + allow(hipchat).to receive_messages( + project_id: project.id, + project: project, + room: 123456, + server: server_url, + token: token + ) + WebMock.stub_request(:post, api_url) + end + + it 'should use v1 if version is provided' do + allow(hipchat).to receive(:api_version).and_return('v1') + expect(HipChat::Client).to receive(:new). + with(token, + api_version: 'v1', + server_url: server_url). + and_return( + double(:hipchat_service).as_null_object) + hipchat.execute(push_sample_data) + end + + it 'should use v2 as the version when nothing is provided' do + allow(hipchat).to receive(:api_version).and_return('') + expect(HipChat::Client).to receive(:new). + with(token, + api_version: 'v2', + server_url: server_url). + and_return( + double(:hipchat_service).as_null_object) + hipchat.execute(push_sample_data) + end + + context 'push events' do + it "should call Hipchat API for push events" do + hipchat.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should create a push message" do + message = hipchat.send(:create_push_message, push_sample_data) + + obj_attr = push_sample_data[:object_attributes] + branch = push_sample_data[:ref].gsub('refs/heads/', '') + expect(message).to include("#{user.name} pushed to branch " \ + "<a href=\"#{project.web_url}/commits/#{branch}\">#{branch}</a> of " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>") + end + end + + context 'tag_push events' do + let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, Gitlab::Git::BLANK_SHA, '1' * 40, 'refs/tags/test', []) } + + it "should call Hipchat API for tag push events" do + hipchat.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should create a tag push message" do + message = hipchat.send(:create_push_message, push_sample_data) + + obj_attr = push_sample_data[:object_attributes] + expect(message).to eq("#{user.name} pushed new tag " \ + "<a href=\"#{project.web_url}/commits/test\">test</a> to " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>\n") + end + end + + context 'issue events' do + let(:issue) { create(:issue, title: 'Awesome issue', description: 'please fix') } + let(:issue_service) { Issues::CreateService.new(project, user) } + let(:issues_sample_data) { issue_service.hook_data(issue, 'open') } + + it "should call Hipchat API for issue events" do + hipchat.execute(issues_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should create an issue message" do + message = hipchat.send(:create_issue_message, issues_sample_data) + + obj_attr = issues_sample_data[:object_attributes] + expect(message).to eq("#{user.name} opened " \ + "<a href=\"#{obj_attr[:url]}\">issue ##{obj_attr["iid"]}</a> in " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ + "<b>Awesome issue</b>" \ + "<pre>please fix</pre>") + end + end + + context 'merge request events' do + let(:merge_request) { create(:merge_request, description: 'please fix', title: 'Awesome merge request', target_project: project, source_project: project) } + let(:merge_service) { MergeRequests::CreateService.new(project, user) } + let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') } + + it "should call Hipchat API for merge requests events" do + hipchat.execute(merge_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should create a merge request message" do + message = hipchat.send(:create_merge_request_message, + merge_sample_data) + + obj_attr = merge_sample_data[:object_attributes] + expect(message).to eq("#{user.name} opened " \ + "<a href=\"#{obj_attr[:url]}\">merge request ##{obj_attr["iid"]}</a> in " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ + "<b>Awesome merge request</b>" \ + "<pre>please fix</pre>") + end + end + + 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 + + message = hipchat.send(:create_message, data) + + 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 + + it "should call Hipchat API for merge request comment events" do + data = Gitlab::NoteDataBuilder.build(merge_request_note, user) + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + + 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 + + it "should call Hipchat API for issue comment events" do + data = Gitlab::NoteDataBuilder.build(issue_note, user) + hipchat.execute(data) + + message = hipchat.send(:create_message, data) + + 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 + + it "should call Hipchat API for snippet comment events" do + data = Gitlab::NoteDataBuilder.build(snippet_note, user) + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + + message = hipchat.send(:create_message, data) + + 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 + + context "#message_options" do + it "should be set to the defaults" do + expect(hipchat.send(:message_options)).to eq({notify: false, color: 'yellow'}) + end + + it "should set notfiy to true" do + allow(hipchat).to receive(:notify).and_return('1') + expect(hipchat.send(:message_options)).to eq({notify: true, color: 'yellow'}) + end + + it "should set the color" do + allow(hipchat).to receive(:color).and_return('red') + expect(hipchat.send(:message_options)).to eq({notify: false, color: 'red'}) + end + end + end +end diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb new file mode 100644 index 00000000000..37690434ec8 --- /dev/null +++ b/spec/models/project_services/irker_service_spec.rb @@ -0,0 +1,109 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' +require 'socket' +require 'json' + +describe IrkerService do + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + before do + subject.active = true + subject.properties['recipients'] = _recipients + end + + context 'active' do + let(:_recipients) { nil } + it { should validate_presence_of :recipients } + end + + context 'too many recipients' do + let(:_recipients) { 'a b c d' } + it 'should add an error if there is too many recipients' do + subject.send :check_recipients_count + expect(subject.errors).not_to be_blank + end + end + + context '3 recipients' do + let(:_recipients) { 'a b c' } + it 'should not add an error if there is 3 recipients' do + subject.send :check_recipients_count + expect(subject.errors).to be_blank + end + end + end + + describe 'Execute' do + let(:irker) { IrkerService.new } + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + + let(:recipients) { '#commits' } + let(:colorize_messages) { '1' } + + before do + allow(irker).to receive_messages( + active: true, + project: project, + project_id: project.id, + service_hook: true, + properties: { + 'recipients' => recipients, + 'colorize_messages' => colorize_messages + } + ) + irker.settings = { + server_ip: 'localhost', + server_port: 6659, + max_channels: 3, + default_irc_uri: 'irc://chat.freenode.net/' + } + irker.valid? + @irker_server = TCPServer.new 'localhost', 6659 + end + + after do + @irker_server.close + end + + it 'should send valid JSON messages to an Irker listener' do + irker.execute(sample_data) + + conn = @irker_server.accept + conn.readlines.each do |line| + msg = JSON.load(line.chomp("\n")) + expect(msg.keys).to match_array(['to', 'privmsg']) + if msg['to'].is_a?(String) + expect(msg['to']).to eq 'irc://chat.freenode.net/#commits' + else + expect(msg['to']).to match_array(['irc://chat.freenode.net/#commits']) + end + end + conn.close + end + end +end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb new file mode 100644 index 00000000000..ddd2cce212c --- /dev/null +++ b/spec/models/project_services/jira_service_spec.rb @@ -0,0 +1,103 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe JiraService do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe "Validations" do + context "active" do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of :project_url } + it { is_expected.to validate_presence_of :issues_url } + it { is_expected.to validate_presence_of :new_issue_url } + end + end + + describe 'description and title' do + let(:project) { create(:project) } + + context 'when it is not set' do + before do + @service = project.create_jira_service(active: true) + end + + after do + @service.destroy! + end + + it 'should be initialized' do + expect(@service.title).to eq('JIRA') + expect(@service.description).to eq("Jira issue tracker") + end + end + + context 'when it is set' do + before do + properties = { 'title' => 'Jira One', 'description' => 'Jira One issue tracker' } + @service = project.create_jira_service(active: true, properties: properties) + end + + after do + @service.destroy! + end + + it "should be correct" do + expect(@service.title).to eq('Jira One') + expect(@service.description).to eq('Jira One issue tracker') + end + end + end + + describe 'project and issue urls' do + let(:project) { create(:project) } + + context 'when gitlab.yml was initialized' do + before do + settings = { "jira" => { + "title" => "Jira", + "project_url" => "http://jira.sample/projects/project_a", + "issues_url" => "http://jira.sample/issues/:id", + "new_issue_url" => "http://jira.sample/projects/project_a/issues/new" + } + } + allow(Gitlab.config).to receive(:issues_tracker).and_return(settings) + @service = project.create_jira_service(active: true) + end + + after do + @service.destroy! + end + + it 'should be prepopulated with the settings' do + expect(@service.properties[:project_url]).to eq('http://jira.sample/projects/project_a') + expect(@service.properties[:issues_url]).to eq("http://jira.sample/issues/:id") + expect(@service.properties[:new_issue_url]).to eq("http://jira.sample/projects/project_a/issues/new") + end + end + end +end diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb new file mode 100644 index 00000000000..ac10ffbd39b --- /dev/null +++ b/spec/models/project_services/pushover_service_spec.rb @@ -0,0 +1,75 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe PushoverService do + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'active' do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of :api_key } + it { is_expected.to validate_presence_of :user_key } + it { is_expected.to validate_presence_of :priority } + end + end + + describe 'Execute' do + let(:pushover) { PushoverService.new } + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + + let(:api_key) { 'verySecret' } + let(:user_key) { 'verySecret' } + let(:device) { 'myDevice' } + let(:priority) { 0 } + let(:sound) { 'bike' } + let(:api_url) { 'https://api.pushover.net/1/messages.json' } + + before do + allow(pushover).to receive_messages( + project: project, + project_id: project.id, + service_hook: true, + api_key: api_key, + user_key: user_key, + device: device, + priority: priority, + sound: sound + ) + + WebMock.stub_request(:post, api_url) + end + + it 'should call Pushover API' do + pushover.execute(sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + end +end diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb new file mode 100644 index 00000000000..8bca1fef44c --- /dev/null +++ b/spec/models/project_services/slack_service/issue_message_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe SlackService::IssueMessage do + subject { SlackService::IssueMessage.new(args) } + + let(:args) { + { + user: { + name: 'Test User', + username: 'Test User' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + + object_attributes: { + title: 'Issue title', + id: 10, + iid: 100, + assignee_id: 1, + url: 'url', + action: 'open', + state: 'opened', + description: 'issue description' + } + } + } + + let(:color) { '#345' } + + 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*') + expect(subject.attachments).to eq([ + { + text: "issue description", + color: color, + } + ]) + end + end + + context 'close' do + before 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*') + expect(subject.attachments).to be_empty + end + end +end diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb new file mode 100644 index 00000000000..aeb408aa766 --- /dev/null +++ b/spec/models/project_services/slack_service/merge_message_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe SlackService::MergeMessage do + subject { SlackService::MergeMessage.new(args) } + + let(:args) { + { + user: { + name: 'Test User', + username: 'Test User' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + + object_attributes: { + title: "Issue title\nSecond line", + id: 10, + iid: 100, + assignee_id: 1, + url: 'url', + state: 'opened', + description: 'issue description', + source_branch: 'source_branch', + target_branch: 'target_branch', + } + } + } + + let(:color) { '#345' } + + context 'open' do + it 'returns a message regarding opening of merge requests' do + expect(subject.pretext).to eq( + 'Test User opened <somewhere.com/merge_requests/100|merge request #100> '\ + 'in <somewhere.com|project_name>: *Issue title*') + expect(subject.attachments).to be_empty + end + end + + context 'close' do + before do + args[:object_attributes][:state] = 'closed' + end + it 'returns a message regarding closing of merge requests' do + expect(subject.pretext).to eq( + 'Test User closed <somewhere.com/merge_requests/100|merge request #100> '\ + 'in <somewhere.com|project_name>: *Issue title*') + expect(subject.attachments).to be_empty + end + 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 new file mode 100644 index 00000000000..21fb575480b --- /dev/null +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe SlackService::NoteMessage do + let(:color) { '#345' } + + before do + @args = { + user: { + name: 'Test User', + username: 'username', + avatar_url: 'http://fakeavatar' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + repository: { + name: 'project_name', + url: 'somewhere.com', + }, + object_attributes: { + id: 10, + note: 'comment on a commit', + url: 'url', + noteable_type: 'Commit' + } + } + end + + context 'commit notes' do + before do + @args[:object_attributes][:note] = 'comment on a commit' + @args[:object_attributes][:noteable_type] = 'Commit' + @args[:commit] = { + id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23', + message: "Added a commit message\ndetails\n123\n" + } + end + + it 'returns a message regarding notes on commits' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq("Test User commented on " \ + "<url|commit 5f163b2b> in <somewhere.com|project_name>: " \ + "*Added a commit message*") + expected_attachments = [ + { + text: "comment on a commit", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'merge request notes' do + before do + @args[:object_attributes][:note] = 'comment on a merge request' + @args[:object_attributes][:noteable_type] = 'MergeRequest' + @args[:merge_request] = { + id: 1, + iid: 30, + title: "merge request title\ndetails\n" + } + end + it 'returns a message regarding notes on a merge request' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq("Test User commented on " \ + "<url|merge request #30> in <somewhere.com|project_name>: " \ + "*merge request title*") + expected_attachments = [ + { + text: "comment on a merge request", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'issue notes' do + before do + @args[:object_attributes][:note] = 'comment on an issue' + @args[:object_attributes][:noteable_type] = 'Issue' + @args[:issue] = { + id: 1, + iid: 20, + title: "issue title\ndetails\n" + } + end + + it 'returns a message regarding notes on an issue' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq( + "Test User commented on " \ + "<url|issue #20> in <somewhere.com|project_name>: " \ + "*issue title*") + expected_attachments = [ + { + text: "comment on an issue", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'project snippet notes' do + before do + @args[:object_attributes][:note] = 'comment on a snippet' + @args[:object_attributes][:noteable_type] = 'Snippet' + @args[:snippet] = { + id: 5, + title: "snippet title\ndetails\n" + } + end + + it 'returns a message regarding notes on a project snippet' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq("Test User commented on " \ + "<url|snippet #5> in <somewhere.com|project_name>: " \ + "*snippet title*") + expected_attachments = [ + { + text: "comment on a snippet", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end +end diff --git a/spec/models/slack_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb index c530fad619b..10963481a12 100644 --- a/spec/models/slack_message_spec.rb +++ b/spec/models/project_services/slack_service/push_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackMessage do - subject { SlackMessage.new(args) } +describe SlackService::PushMessage do + subject { SlackService::PushMessage.new(args) } let(:args) { { @@ -25,41 +25,64 @@ describe SlackMessage do end it 'returns a message regarding pushes' do - subject.pretext.should == + expect(subject.pretext).to eq( 'user_name pushed to branch <url/commits/master|master> of '\ '<url|project_name> (<url/compare/before...after|Compare changes>)' - subject.attachments.should == [ + ) + expect(subject.attachments).to eq([ { - text: "<url1|abcdefghi>: message1 - author1\n"\ - "<url2|123456789>: message2 - author2", + text: "<url1|abcdefgh>: message1 - author1\n"\ + "<url2|12345678>: message2 - author2", color: color, } - ] + ]) + end + end + + context 'tag push' do + let(:args) { + { + after: 'after', + before: Gitlab::Git::BLANK_SHA, + project_name: 'project_name', + ref: 'refs/tags/new_tag', + user_name: 'user_name', + project_url: 'url' + } + } + + it 'returns a message regarding pushes' do + expect(subject.pretext).to eq('user_name pushed new tag ' \ + '<url/commits/new_tag|new_tag> to ' \ + '<url|project_name>') + expect(subject.attachments).to be_empty end end context 'new branch' do before do - args[:before] = '000000' + args[:before] = Gitlab::Git::BLANK_SHA end it 'returns a message regarding a new branch' do - subject.pretext.should == + expect(subject.pretext).to eq( 'user_name pushed new branch <url/commits/master|master> to '\ '<url|project_name>' - subject.attachments.should be_empty + ) + expect(subject.attachments).to be_empty end end context 'removed branch' do before do - args[:after] = '000000' + args[:after] = Gitlab::Git::BLANK_SHA end it 'returns a message regarding a removed branch' do - subject.pretext.should == + expect(subject.pretext).to eq( 'user_name removed branch master from <url|project_name>' - subject.attachments.should be_empty + ) + expect(subject.attachments).to be_empty end end end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb new file mode 100644 index 00000000000..69466b11f09 --- /dev/null +++ b/spec/models/project_services/slack_service_spec.rb @@ -0,0 +1,171 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe SlackService do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe "Validations" do + context "active" do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of :webhook } + end + end + + describe "Execute" do + let(:slack) { SlackService.new } + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } + let(:username) { 'slack_username' } + let(:channel) { 'slack_channel' } + + before do + allow(slack).to receive_messages( + project: project, + project_id: project.id, + service_hook: true, + webhook: webhook_url + ) + + WebMock.stub_request(:post, webhook_url) + + opts = { + title: 'Awesome issue', + description: 'please fix' + } + + issue_service = Issues::CreateService.new(project, user, opts) + @issue = issue_service.execute + @issues_sample_data = issue_service.hook_data(@issue, 'open') + + opts = { + title: 'Awesome merge_request', + description: 'please fix', + source_branch: 'stable', + target_branch: 'master' + } + merge_service = MergeRequests::CreateService.new(project, + user, opts) + @merge_request = merge_service.execute + @merge_sample_data = merge_service.hook_data(@merge_request, + 'open') + end + + it "should call Slack API for push events" do + slack.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "should call Slack API for issue events" do + slack.execute(@issues_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "should call Slack API for merge requests events" do + slack.execute(@merge_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it 'should use the username as an option for slack when configured' do + allow(slack).to receive(:username).and_return(username) + expect(Slack::Notifier).to receive(:new). + with(webhook_url, username: username). + and_return( + double(:slack_service).as_null_object + ) + slack.execute(push_sample_data) + end + + it 'should use the channel as an option when it is configured' do + allow(slack).to receive(:channel).and_return(channel) + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: channel). + and_return( + double(:slack_service).as_null_object + ) + slack.execute(push_sample_data) + end + end + + describe "Note events" 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 + allow(slack).to receive_messages( + project: project, + project_id: project.id, + service_hook: true, + webhook: webhook_url + ) + + 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) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + 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 + + 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 + + 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 diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index a6e1d9eef50..3e8f106d27f 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -19,13 +19,13 @@ require 'spec_helper' describe ProjectSnippet do describe "Associations" do - it { should belong_to(:project) } + it { is_expected.to belong_to(:project) } end describe "Mass assignment" do end describe "Validation" do - it { should validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:project) } end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 70a15cac1a8..63091e913ff 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -24,107 +24,168 @@ # 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) +# avatar :string(255) # require 'spec_helper' describe Project do - describe "Associations" do - it { should belong_to(:group) } - it { should belong_to(:namespace) } - it { should belong_to(:creator).class_name('User') } - it { should have_many(:users) } - it { should have_many(:events).dependent(:destroy) } - it { should have_many(:merge_requests).dependent(:destroy) } - it { should have_many(:issues).dependent(:destroy) } - it { should have_many(:milestones).dependent(:destroy) } - it { should have_many(:project_members).dependent(:destroy) } - it { should have_many(:notes).dependent(:destroy) } - it { should have_many(:snippets).class_name('ProjectSnippet').dependent(:destroy) } - it { should have_many(:deploy_keys_projects).dependent(:destroy) } - it { should have_many(:deploy_keys) } - it { should have_many(:hooks).dependent(:destroy) } - it { should have_many(:protected_branches).dependent(:destroy) } - it { should have_one(:forked_project_link).dependent(:destroy) } - it { should have_one(:slack_service).dependent(:destroy) } - it { should have_one(:pushover_service).dependent(:destroy) } + describe 'associations' do + it { is_expected.to belong_to(:group) } + it { is_expected.to belong_to(:namespace) } + it { is_expected.to belong_to(:creator).class_name('User') } + it { is_expected.to have_many(:users) } + it { is_expected.to have_many(:events).dependent(:destroy) } + it { is_expected.to have_many(:merge_requests).dependent(:destroy) } + it { is_expected.to have_many(:issues).dependent(:destroy) } + it { is_expected.to have_many(:milestones).dependent(:destroy) } + it { is_expected.to have_many(:project_members).dependent(:destroy) } + it { is_expected.to have_many(:notes).dependent(:destroy) } + it { is_expected.to have_many(:snippets).class_name('ProjectSnippet').dependent(:destroy) } + it { is_expected.to have_many(:deploy_keys_projects).dependent(:destroy) } + it { is_expected.to have_many(:deploy_keys) } + it { is_expected.to have_many(:hooks).dependent(:destroy) } + it { is_expected.to have_many(:protected_branches).dependent(:destroy) } + it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } + it { is_expected.to have_one(:slack_service).dependent(:destroy) } + it { is_expected.to have_one(:pushover_service).dependent(:destroy) } + it { is_expected.to have_one(:asana_service).dependent(:destroy) } end - describe "Mass assignment" do + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Gitlab::ConfigHelper) } + it { is_expected.to include_module(Gitlab::ShellAdapter) } + it { is_expected.to include_module(Gitlab::VisibilityLevel) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } end - describe "Validation" do + describe 'validation' do let!(:project) { create(:project) } - it { should validate_presence_of(:name) } - it { should validate_uniqueness_of(:name).scoped_to(:namespace_id) } - it { should ensure_length_of(:name).is_within(0..255) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) } + it { is_expected.to validate_length_of(:name).is_within(0..255) } - it { should validate_presence_of(:path) } - it { should validate_uniqueness_of(:path).scoped_to(:namespace_id) } - it { should ensure_length_of(:path).is_within(0..255) } - it { should ensure_length_of(:description).is_within(0..2000) } - it { should validate_presence_of(:creator) } - it { should ensure_length_of(:issues_tracker_id).is_within(0..255) } - it { should validate_presence_of(:namespace) } + it { is_expected.to validate_presence_of(:path) } + it { is_expected.to validate_uniqueness_of(:path).scoped_to(:namespace_id) } + it { is_expected.to validate_length_of(:path).is_within(0..255) } + it { is_expected.to validate_length_of(:description).is_within(0..2000) } + it { is_expected.to validate_presence_of(:creator) } + it { is_expected.to validate_length_of(:issues_tracker_id).is_within(0..255) } + it { is_expected.to validate_presence_of(:namespace) } - it "should not allow new projects beyond user limits" do + it 'should not allow new projects beyond user limits' do project2 = build(:project) - project2.stub(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object) - project2.should_not be_valid - project2.errors[:limit_reached].first.should match(/Your project limit is 0/) + 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/) end end - describe "Respond to" do - it { should respond_to(:url_to_repo) } - it { should respond_to(:repo_exists?) } - it { should respond_to(:satellite) } - it { should respond_to(:update_merge_requests) } - it { should respond_to(:execute_hooks) } - it { should respond_to(:name_with_namespace) } - it { should respond_to(:owner) } - it { should respond_to(:path_with_namespace) } + describe 'Respond to' do + it { is_expected.to respond_to(:url_to_repo) } + it { is_expected.to respond_to(:repo_exists?) } + it { is_expected.to respond_to(:satellite) } + it { is_expected.to respond_to(:update_merge_requests) } + it { is_expected.to respond_to(:execute_hooks) } + it { is_expected.to respond_to(:name_with_namespace) } + it { is_expected.to respond_to(:owner) } + it { is_expected.to respond_to(:path_with_namespace) } + end + + describe '#to_reference' do + let(:project) { create(:empty_project) } + + it 'returns a String reference to the object' do + expect(project.to_reference).to eq project.path_with_namespace + end end - it "should return valid url to repo" do - project = Project.new(path: "somewhere") - project.url_to_repo.should == Gitlab.config.gitlab_shell.ssh_path_prefix + "somewhere.git" + it 'should return valid url to repo' do + project = Project.new(path: 'somewhere') + expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git') end - it "returns the full web URL for this repo" do - project = Project.new(path: "somewhere") - project.web_url.should == "#{Gitlab.config.gitlab.url}/somewhere" + it 'returns the full web URL for this repo' do + project = Project.new(path: 'somewhere') + expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/somewhere") end - it "returns the web URL without the protocol for this repo" do - project = Project.new(path: "somewhere") - project.web_url_without_protocol.should == "#{Gitlab.config.gitlab.url.split("://")[1]}/somewhere" + it 'returns the web URL without the protocol for this repo' do + project = Project.new(path: 'somewhere') + expect(project.web_url_without_protocol).to eq("#{Gitlab.config.gitlab.url.split('://')[1]}/somewhere") end - describe "last_activity methods" do + describe 'last_activity methods' do let(:project) { create(:project) } let(:last_event) { double(created_at: Time.now) } - describe "last_activity" do - it "should alias last_activity to last_event" do - project.stub(last_event: last_event) - project.last_activity.should == last_event + describe 'last_activity' do + it 'should alias last_activity to last_event' do + allow(project).to receive(:last_event).and_return(last_event) + expect(project.last_activity).to eq(last_event) end end describe 'last_activity_date' do it 'returns the creation date of the project\'s last event if present' do last_activity_event = create(:event, project: project) - project.last_activity_at.to_i.should == last_event.created_at.to_i + expect(project.last_activity_at.to_i).to eq(last_event.created_at.to_i) end it 'returns the project\'s last update date if it has no events' do - project.last_activity_date.should == project.updated_at + expect(project.last_activity_date).to eq(project.updated_at) + end + end + end + + describe '#get_issue' do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + + context 'with default issues tracker' do + it 'returns an issue' do + expect(project.get_issue(issue.iid)).to eq issue + end + + it 'returns nil when no issue found' do + expect(project.get_issue(999)).to be_nil + end + end + + context 'with external issues tracker' do + before do + allow(project).to receive(:default_issues_tracker?).and_return(false) + end + + it 'returns an ExternalIssue' do + issue = project.get_issue('FOO-1234') + expect(issue).to be_kind_of(ExternalIssue) + expect(issue.iid).to eq 'FOO-1234' + expect(issue.project).to eq project end end end + describe '#issue_exists?' do + let(:project) { create(:empty_project) } + + it 'is truthy when issue exists' do + expect(project).to receive(:get_issue).and_return(double) + expect(project.issue_exists?(1)).to be_truthy + end + + it 'is falsey when issue does not exist' do + expect(project).to receive(:get_issue).and_return(nil) + expect(project.issue_exists?(1)).to be_falsey + end + end + describe :update_merge_requests do let(:project) { create(:project) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } @@ -132,16 +193,16 @@ describe Project do let(:prev_commit_id) { merge_request.commits.last.id } let(:commit_id) { merge_request.commits.first.id } - it "should close merge request if last commit from source branch was pushed to target branch" do + it 'should close merge request if last commit from source branch was pushed to target branch' do project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.target_branch}", key.user) merge_request.reload - merge_request.merged?.should be_true + expect(merge_request.merged?).to be_truthy end - it "should update merge request commits with new one if pushed to source branch" do + it 'should update merge request commits with new one if pushed to source branch' do project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.source_branch}", key.user) merge_request.reload - merge_request.last_commit.id.should == commit_id + expect(merge_request.last_commit.id).to eq(commit_id) end end @@ -152,8 +213,8 @@ describe Project do @project = create(:project, name: 'gitlabhq', namespace: @group) end - it { Project.find_with_namespace('gitlab/gitlabhq').should == @project } - it { Project.find_with_namespace('gitlab-ci').should be_nil } + it { expect(Project.find_with_namespace('gitlab/gitlabhq')).to eq(@project) } + it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil } end end @@ -164,47 +225,28 @@ describe Project do @project = create(:project, name: 'gitlabhq', namespace: @group) end - it { @project.to_param.should == "gitlab/gitlabhq" } + it { expect(@project.to_param).to eq('gitlabhq') } end end describe :repository do let(:project) { create(:project) } - it "should return valid repo" do - project.repository.should be_kind_of(Repository) + it 'should return valid repo' do + expect(project.repository).to be_kind_of(Repository) end end - describe :issue_exists? do - let(:project) { create(:project) } - let(:existed_issue) { create(:issue, project: project) } - let(:not_existed_issue) { create(:issue) } - let(:ext_project) { create(:redmine_project) } - - it "should be true or if used internal tracker and issue exists" do - project.issue_exists?(existed_issue.iid).should be_true - end - - it "should be false or if used internal tracker and issue not exists" do - project.issue_exists?(not_existed_issue.iid).should be_false - end - - it "should always be true if used other tracker" do - ext_project.issue_exists?(rand(100)).should be_true - end - end - - describe :used_default_issues_tracker? do + describe :default_issues_tracker? do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } it "should be true if used internal tracker" do - project.used_default_issues_tracker?.should be_true + expect(project.default_issues_tracker?).to be_truthy end it "should be false if used other tracker" do - ext_project.used_default_issues_tracker?.should be_false + expect(ext_project.default_issues_tracker?).to be_falsey end end @@ -212,20 +254,20 @@ describe Project do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } - it "should be true for projects with external issues tracker if issues enabled" do - ext_project.can_have_issues_tracker_id?.should be_true + it 'should be true for projects with external issues tracker if issues enabled' do + expect(ext_project.can_have_issues_tracker_id?).to be_truthy end - it "should be false for projects with internal issue tracker if issues enabled" do - project.can_have_issues_tracker_id?.should be_false + it 'should be false for projects with internal issue tracker if issues enabled' do + expect(project.can_have_issues_tracker_id?).to be_falsey end - it "should be always false if issues disabled" do + it 'should be always false if issues disabled' do project.issues_enabled = false ext_project.issues_enabled = false - project.can_have_issues_tracker_id?.should be_false - ext_project.can_have_issues_tracker_id?.should be_false + expect(project.can_have_issues_tracker_id?).to be_falsey + expect(ext_project.can_have_issues_tracker_id?).to be_falsey end end @@ -236,8 +278,8 @@ describe Project do project.protected_branches.create(name: 'master') end - it { project.open_branches.map(&:name).should include('feature') } - it { project.open_branches.map(&:name).should_not include('master') } + it { expect(project.open_branches.map(&:name)).to include('feature') } + it { expect(project.open_branches.map(&:name)).not_to include('master') } end describe '#star_count' do @@ -308,4 +350,49 @@ describe Project do expect(project.star_count).to eq(0) end end + + describe :avatar_type do + let(:project) { create(:project) } + + it 'should be true if avatar is image' do + project.update_attribute(:avatar, 'uploads/avatar.png') + expect(project.avatar_type).to be_truthy + end + + it 'should be false if avatar is html page' do + project.update_attribute(:avatar, 'uploads/avatar.html') + expect(project.avatar_type).to eq(['only images allowed']) + end + end + + describe :avatar_url do + subject { project.avatar_url } + + let(:project) { create(:project) } + + context 'When avatar file is uploaded' do + before do + project.update_columns(avatar: 'uploads/avatar.png') + allow(project.avatar).to receive(:present?) { true } + end + + let(:avatar_path) do + "/uploads/project/avatar/#{project.id}/uploads/avatar.png" + end + + it { should eq "http://localhost#{avatar_path}" } + end + + context 'When avatar file in git' do + before do + allow(project).to receive(:avatar_in_git) { true } + end + + let(:avatar_path) do + "/#{project.namespace.name}/#{project.path}/avatar" + end + + it { should eq "http://localhost#{avatar_path}" } + end + end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index bbf50b654f4..19201cc15a7 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -16,19 +16,19 @@ describe ProjectTeam do end describe 'members collection' do - it { project.team.masters.should include(master) } - it { project.team.masters.should_not include(guest) } - it { project.team.masters.should_not include(reporter) } - it { project.team.masters.should_not include(nonmember) } + it { expect(project.team.masters).to include(master) } + it { expect(project.team.masters).not_to include(guest) } + it { expect(project.team.masters).not_to include(reporter) } + it { expect(project.team.masters).not_to include(nonmember) } end describe 'access methods' do - it { project.team.master?(master).should be_true } - it { project.team.master?(guest).should be_false } - it { project.team.master?(reporter).should be_false } - it { project.team.master?(nonmember).should be_false } - it { project.team.member?(nonmember).should be_false } - it { project.team.member?(guest).should be_true } + it { expect(project.team.master?(master)).to be_truthy } + it { expect(project.team.master?(guest)).to be_falsey } + it { expect(project.team.master?(reporter)).to be_falsey } + it { expect(project.team.master?(nonmember)).to be_falsey } + it { expect(project.team.member?(nonmember)).to be_falsey } + it { expect(project.team.member?(guest)).to be_truthy } end end @@ -49,21 +49,21 @@ describe ProjectTeam do end describe 'members collection' do - it { project.team.reporters.should include(reporter) } - it { project.team.masters.should include(master) } - it { project.team.masters.should include(guest) } - it { project.team.masters.should_not include(reporter) } - it { project.team.masters.should_not include(nonmember) } + it { expect(project.team.reporters).to include(reporter) } + it { expect(project.team.masters).to include(master) } + it { expect(project.team.masters).to include(guest) } + it { expect(project.team.masters).not_to include(reporter) } + it { expect(project.team.masters).not_to include(nonmember) } end describe 'access methods' do - it { project.team.reporter?(reporter).should be_true } - it { project.team.master?(master).should be_true } - it { project.team.master?(guest).should be_true } - it { project.team.master?(reporter).should be_false } - it { project.team.master?(nonmember).should be_false } - it { project.team.member?(nonmember).should be_false } - it { project.team.member?(guest).should be_true } + it { expect(project.team.reporter?(reporter)).to be_truthy } + it { expect(project.team.master?(master)).to be_truthy } + it { expect(project.team.master?(guest)).to be_truthy } + it { expect(project.team.master?(reporter)).to be_falsey } + it { expect(project.team.master?(nonmember)).to be_falsey } + it { expect(project.team.member?(nonmember)).to be_falsey } + it { expect(project.team.member?(guest)).to be_truthy } end end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index e4ee2fc5b13..2acdb7dfddc 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -12,19 +12,19 @@ describe ProjectWiki do describe "#path_with_namespace" do it "returns the project path with namespace with the .wiki extension" do - subject.path_with_namespace.should == project.path_with_namespace + ".wiki" + expect(subject.path_with_namespace).to eq(project.path_with_namespace + ".wiki") end end describe "#url_to_repo" do it "returns the correct ssh url to the repo" do - subject.url_to_repo.should == gitlab_shell.url_to_repo(subject.path_with_namespace) + expect(subject.url_to_repo).to eq(gitlab_shell.url_to_repo(subject.path_with_namespace)) end end describe "#ssh_url_to_repo" do it "equals #url_to_repo" do - subject.ssh_url_to_repo.should == subject.url_to_repo + expect(subject.ssh_url_to_repo).to eq(subject.url_to_repo) end end @@ -32,21 +32,21 @@ describe ProjectWiki do it "provides the full http url to the repo" do gitlab_url = Gitlab.config.gitlab.url repo_http_url = "#{gitlab_url}/#{subject.path_with_namespace}.git" - subject.http_url_to_repo.should == repo_http_url + expect(subject.http_url_to_repo).to eq(repo_http_url) end end describe "#wiki" do it "contains a Gollum::Wiki instance" do - subject.wiki.should be_a Gollum::Wiki + expect(subject.wiki).to be_a Gollum::Wiki end it "creates a new wiki repo if one does not yet exist" do - project_wiki.create_page("index", "test content").should be_true + expect(project_wiki.create_page("index", "test content")).to be_truthy end it "raises CouldNotCreateWikiError if it can't create the wiki repository" do - project_wiki.stub(:init_repo).and_return(false) + allow(project_wiki).to receive(:init_repo).and_return(false) expect { project_wiki.send(:create_repo!) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError) end end @@ -54,21 +54,27 @@ describe ProjectWiki do describe "#empty?" do context "when the wiki repository is empty" do before do - Gitlab::Shell.any_instance.stub(:add_repository) do + allow_any_instance_of(Gitlab::Shell).to receive(:add_repository) do create_temp_repo("#{Rails.root}/tmp/test-git-base-path/non-existant.wiki.git") end - project.stub(:path_with_namespace).and_return("non-existant") + allow(project).to receive(:path_with_namespace).and_return("non-existant") end - its(:empty?) { should be_true } + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_truthy } + end end context "when the wiki has pages" do before do - create_page("index", "This is an awesome new Gollum Wiki") + project_wiki.create_page("index", "This is an awesome new Gollum Wiki") end - its(:empty?) { should be_false } + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end end end @@ -83,11 +89,11 @@ describe ProjectWiki do end it "returns an array of WikiPage instances" do - @pages.first.should be_a WikiPage + expect(@pages.first).to be_a WikiPage end it "returns the correct number of pages" do - @pages.count.should == 1 + expect(@pages.count).to eq(1) end end @@ -102,55 +108,55 @@ describe ProjectWiki do it "returns the latest version of the page if it exists" do page = subject.find_page("index page") - page.title.should == "index page" + expect(page.title).to eq("index page") end it "returns nil if the page does not exist" do - subject.find_page("non-existant").should == nil + expect(subject.find_page("non-existant")).to eq(nil) end it "can find a page by slug" do page = subject.find_page("index-page") - page.title.should == "index page" + expect(page.title).to eq("index page") end it "returns a WikiPage instance" do page = subject.find_page("index page") - page.should be_a WikiPage + expect(page).to be_a WikiPage end end describe '#find_file' do before do file = Gollum::File.new(subject.wiki) - Gollum::Wiki.any_instance. - stub(:file).with('image.jpg', 'master', true). + allow_any_instance_of(Gollum::Wiki). + to receive(:file).with('image.jpg', 'master', true). and_return(file) - Gollum::File.any_instance. - stub(:mime_type). + allow_any_instance_of(Gollum::File). + to receive(:mime_type). and_return('image/jpeg') - Gollum::Wiki.any_instance. - stub(:file).with('non-existant', 'master', true). + allow_any_instance_of(Gollum::Wiki). + to receive(:file).with('non-existant', 'master', true). and_return(nil) end after do - Gollum::Wiki.any_instance.unstub(:file) - Gollum::File.any_instance.unstub(:mime_type) + allow_any_instance_of(Gollum::Wiki).to receive(:file).and_call_original + allow_any_instance_of(Gollum::File).to receive(:mime_type).and_call_original end it 'returns the latest version of the file if it exists' do file = subject.find_file('image.jpg') - file.mime_type.should == 'image/jpeg' + expect(file.mime_type).to eq('image/jpeg') end it 'returns nil if the page does not exist' do - subject.find_file('non-existant').should == nil + expect(subject.find_file('non-existant')).to eq(nil) end it 'returns a Gollum::File instance' do file = subject.find_file('image.jpg') - file.should be_a Gollum::File + expect(file).to be_a Gollum::File end end @@ -160,23 +166,23 @@ describe ProjectWiki do end it "creates a new wiki page" do - subject.create_page("test page", "this is content").should_not == false - subject.pages.count.should == 1 + expect(subject.create_page("test page", "this is content")).not_to eq(false) + expect(subject.pages.count).to eq(1) end it "returns false when a duplicate page exists" do subject.create_page("test page", "content") - subject.create_page("test page", "content").should == false + expect(subject.create_page("test page", "content")).to eq(false) end it "stores an error message when a duplicate page exists" do 2.times { subject.create_page("test page", "content") } - subject.error_message.should =~ /Duplicate page:/ + expect(subject.error_message).to match(/Duplicate page:/) end it "sets the correct commit message" do subject.create_page("test page", "some content", :markdown, "commit message") - subject.pages.first.page.version.message.should == "commit message" + expect(subject.pages.first.page.version.message).to eq("commit message") end end @@ -193,11 +199,11 @@ describe ProjectWiki do end it "updates the content of the page" do - @page.raw_data.should == "some other content" + expect(@page.raw_data).to eq("some other content") end it "sets the correct commit message" do - @page.version.message.should == "updated page" + expect(@page.version.message).to eq("updated page") end end @@ -209,7 +215,7 @@ describe ProjectWiki do it "deletes the page" do subject.delete_page(@page) - subject.pages.count.should == 0 + expect(subject.pages.count).to eq(0) end end diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index af48c2c6d9e..1e6937b536c 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -2,25 +2,26 @@ # # 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 +# 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 do describe 'Associations' do - it { should belong_to(:project) } + it { is_expected.to belong_to(:project) } end describe "Mass assignment" do end describe 'Validation' do - it { should validate_presence_of(:project) } - it { should validate_presence_of(:name) } + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:name) } end end diff --git a/spec/models/pushover_service_spec.rb b/spec/models/pushover_service_spec.rb deleted file mode 100644 index 59db69d7572..00000000000 --- a/spec/models/pushover_service_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -require 'spec_helper' - -describe PushoverService do - describe 'Associations' do - it { should belong_to :project } - it { should have_one :service_hook } - end - - describe 'Validations' do - context 'active' do - before do - subject.active = true - end - - it { should validate_presence_of :api_key } - it { should validate_presence_of :user_key } - it { should validate_presence_of :priority } - end - end - - describe 'Execute' do - let(:pushover) { PushoverService.new } - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:sample_data) { GitPushService.new.sample_data(project, user) } - - let(:api_key) { 'verySecret' } - let(:user_key) { 'verySecret' } - let(:device) { 'myDevice' } - let(:priority) { 0 } - let(:sound) { 'bike' } - let(:api_url) { 'https://api.pushover.net/1/messages.json' } - - before do - pushover.stub( - project: project, - project_id: project.id, - service_hook: true, - api_key: api_key, - user_key: user_key, - device: device, - priority: priority, - sound: sound - ) - - WebMock.stub_request(:post, api_url) - end - - it 'should call Pushover API' do - pushover.execute(sample_data) - - WebMock.should have_requested(:post, api_url).once - end - end -end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 6c3e221f343..f41e5a97ca3 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -8,14 +8,21 @@ describe Repository do describe :branch_names_contains do subject { repository.branch_names_contains(sample_commit.id) } - it { should include('master') } - it { should_not include('feature') } - it { should_not include('fix') } + it { is_expected.to include('master') } + it { is_expected.not_to include('feature') } + it { is_expected.not_to include('fix') } + end + + describe :tag_names_contains do + subject { repository.tag_names_contains(sample_commit.id) } + + it { is_expected.to include('v1.1.0') } + it { is_expected.not_to include('v1.0.0') } end describe :last_commit_for_path do subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } - it { should eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } + it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } end end diff --git a/spec/models/service_hook_spec.rb b/spec/models/service_hook_spec.rb deleted file mode 100644 index 6ec82438dfe..00000000000 --- a/spec/models/service_hook_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# == Schema Information -# -# Table name: web_hooks -# -# id :integer not null, primary key -# url :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) 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) -# - -require "spec_helper" - -describe ServiceHook do - describe "Associations" do - it { should belong_to :service } - end -end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index c96f2b20529..cb633216d3b 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -2,14 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # require 'spec_helper' @@ -17,8 +23,8 @@ require 'spec_helper' describe Service do describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } end describe "Mass assignment" do @@ -33,14 +39,12 @@ describe Service do let (:project) { create :project } before do - @service.stub( - project: project - ) + allow(@service).to receive(:project).and_return(project) @testable = @service.can_test? end describe :can_test do - it { @testable.should == true } + it { expect(@testable).to eq(true) } end end @@ -48,14 +52,37 @@ describe Service do let (:project) { create :project } before do - @service.stub( - project: project - ) + allow(@service).to receive(:project).and_return(project) @testable = @service.can_test? end describe :can_test do - it { @testable.should == true } + it { expect(@testable).to eq(true) } + end + end + end + + describe "Template" do + describe "for pushover service" do + let(:service_template) { + PushoverService.create(template: true, properties: {device: 'MyDevice', sound: 'mic', priority: 4, api_key: '123456789'}) + } + let(:project) { create(:project) } + + describe 'should be prefilled for projects pushover service' do + before do + service_template + project.build_missing_services + end + + it "should have all fields prefilled" do + service = project.pushover_service + expect(service.template).to eq(false) + expect(service.device).to eq('MyDevice') + expect(service.sound).to eq('mic') + expect(service.priority).to eq(4) + expect(service.api_key).to eq('123456789') + end end end end diff --git a/spec/models/slack_service_spec.rb b/spec/models/slack_service_spec.rb deleted file mode 100644 index d4840391967..00000000000 --- a/spec/models/slack_service_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -require 'spec_helper' - -describe SlackService do - describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } - end - - describe "Validations" do - context "active" do - before do - subject.active = true - end - - it { should validate_presence_of :webhook } - end - end - - describe "Execute" do - let(:slack) { SlackService.new } - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:sample_data) { GitPushService.new.sample_data(project, user) } - let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } - - before do - slack.stub( - project: project, - project_id: project.id, - service_hook: true, - webhook: webhook_url - ) - - WebMock.stub_request(:post, webhook_url) - end - - it "should call Slack API" do - slack.execute(sample_data) - - WebMock.should have_requested(:post, webhook_url).once - end - end -end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 1ef2c512c1f..81581838675 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -18,23 +18,46 @@ require 'spec_helper' describe Snippet do - describe "Associations" do - it { should belong_to(:author).class_name('User') } - it { should have_many(:notes).dependent(:destroy) } + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Gitlab::VisibilityLevel) } + it { is_expected.to include_module(Linguist::BlobHelper) } + it { is_expected.to include_module(Participable) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } end - describe "Mass assignment" do + describe 'associations' do + it { is_expected.to belong_to(:author).class_name('User') } + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:notes).dependent(:destroy) } end - describe "Validation" do - it { should validate_presence_of(:author) } + describe 'validation' do + it { is_expected.to validate_presence_of(:author) } + + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_length_of(:title).is_within(0..255) } + + it { is_expected.to validate_length_of(:file_name).is_within(0..255) } + + it { is_expected.to validate_presence_of(:content) } + + it { is_expected.to validate_inclusion_of(:visibility_level).in_array(Gitlab::VisibilityLevel.values) } + end - it { should validate_presence_of(:title) } - it { should ensure_length_of(:title).is_within(0..255) } + describe '#to_reference' do + let(:project) { create(:empty_project) } + let(:snippet) { create(:snippet, project: project) } - it { should validate_presence_of(:file_name) } - it { should ensure_length_of(:title).is_within(0..255) } + it 'returns a String reference to the object' do + expect(snippet.to_reference).to eq "$#{snippet.id}" + end - it { should validate_presence_of(:content) } + it 'supports a cross-project reference' do + cross = double('project') + expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}" + end end end diff --git a/spec/models/system_hook_spec.rb b/spec/models/system_hook_spec.rb deleted file mode 100644 index 4ab5261dc9d..00000000000 --- a/spec/models/system_hook_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -# == Schema Information -# -# Table name: web_hooks -# -# id :integer not null, primary key -# url :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) 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) -# - -require "spec_helper" - -describe SystemHook do - describe "execute" do - before(:each) do - @system_hook = create(:system_hook) - WebMock.stub_request(:post, @system_hook.url) - end - - it "project_create hook" do - Projects::CreateService.new(create(:user), name: 'empty').execute - WebMock.should have_requested(:post, @system_hook.url).with(body: /project_create/).once - end - - it "project_destroy hook" do - user = create(:user) - project = create(:empty_project, namespace: user.namespace) - Projects::DestroyService.new(project, user, {}).execute - WebMock.should have_requested(:post, @system_hook.url).with(body: /project_destroy/).once - end - - it "user_create hook" do - create(:user) - WebMock.should have_requested(:post, @system_hook.url).with(body: /user_create/).once - end - - it "user_destroy hook" do - user = create(:user) - user.destroy - WebMock.should have_requested(:post, @system_hook.url).with(body: /user_destroy/).once - end - - it "project_create hook" do - user = create(:user) - project = create(:project) - project.team << [user, :master] - WebMock.should have_requested(:post, @system_hook.url).with(body: /user_add_to_team/).once - end - - it "project_destroy hook" do - user = create(:user) - project = create(:project) - project.team << [user, :master] - project.project_members.destroy_all - WebMock.should have_requested(:post, @system_hook.url).with(body: /user_remove_from_team/).once - end - end -end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6d865cfc691..9f7c83f3476 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,79 +2,102 @@ # # 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 -# extern_uid :string(255) -# provider :string(255) -# 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 +# 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 +# github_access_token :string(255) +# gitlab_access_token :string(255) +# notification_email :string(255) +# hide_no_password :boolean default(FALSE) +# password_automatically_set :boolean default(FALSE) +# bitbucket_access_token :string(255) +# bitbucket_access_token_secret :string(255) +# location :string(255) +# public_email :string(255) default(""), not null +# encrypted_otp_secret :string(255) +# encrypted_otp_secret_iv :string(255) +# encrypted_otp_secret_salt :string(255) +# otp_required_for_login :boolean +# otp_backup_codes :text +# dashboard :integer default(0) # require 'spec_helper' describe User do - describe "Associations" do - it { should have_one(:namespace) } - it { should have_many(:snippets).class_name('Snippet').dependent(:destroy) } - it { should have_many(:project_members).dependent(:destroy) } - it { should have_many(:groups) } - it { should have_many(:keys).dependent(:destroy) } - it { should have_many(:events).class_name('Event').dependent(:destroy) } - it { should have_many(:recent_events).class_name('Event') } - it { should have_many(:issues).dependent(:destroy) } - it { should have_many(:notes).dependent(:destroy) } - it { should have_many(:assigned_issues).dependent(:destroy) } - it { should have_many(:merge_requests).dependent(:destroy) } - it { should have_many(:assigned_merge_requests).dependent(:destroy) } + include Gitlab::CurrentSettings + + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Gitlab::ConfigHelper) } + it { is_expected.to include_module(Gitlab::CurrentSettings) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } + it { is_expected.to include_module(TokenAuthenticatable) } end - describe "Mass assignment" do + describe 'associations' do + it { is_expected.to have_one(:namespace) } + it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) } + it { is_expected.to have_many(:project_members).dependent(:destroy) } + it { is_expected.to have_many(:groups) } + it { is_expected.to have_many(:keys).dependent(:destroy) } + it { is_expected.to have_many(:events).class_name('Event').dependent(:destroy) } + it { is_expected.to have_many(:recent_events).class_name('Event') } + it { is_expected.to have_many(:issues).dependent(:destroy) } + it { is_expected.to have_many(:notes).dependent(:destroy) } + it { is_expected.to have_many(:assigned_issues).dependent(:destroy) } + it { is_expected.to have_many(:merge_requests).dependent(:destroy) } + it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) } + it { is_expected.to have_many(:identities).dependent(:destroy) } end describe 'validations' do - it { should validate_presence_of(:username) } - it { should validate_presence_of(:projects_limit) } - it { should validate_numericality_of(:projects_limit) } - it { should allow_value(0).for(:projects_limit) } - it { should_not allow_value(-1).for(:projects_limit) } + it { is_expected.to validate_presence_of(:username) } + it { is_expected.to validate_presence_of(:projects_limit) } + it { is_expected.to validate_numericality_of(:projects_limit) } + it { is_expected.to allow_value(0).for(:projects_limit) } + it { is_expected.not_to allow_value(-1).for(:projects_limit) } - it { should ensure_length_of(:bio).is_within(0..255) } + it { is_expected.to validate_length_of(:bio).is_within(0..255) } describe 'email' do it 'accepts info@example.com' do @@ -106,38 +129,115 @@ describe User do user = build(:user, email: "lol!'+=?><#$%^&*()@gmail.com") expect(user).to be_invalid end + + context 'when no signup domains listed' do + before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return([]) } + it 'accepts any email' do + user = build(:user, email: "info@example.com") + expect(user).to be_valid + end + 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']) } + it 'accepts info@example.com' do + user = build(:user, email: "info@example.com") + expect(user).to be_valid + end + + it 'accepts info@test.example.com' do + user = build(:user, email: "info@test.example.com") + expect(user).to be_valid + end + + it 'rejects example@test.com' do + user = build(:user, email: "example@test.com") + expect(user).to be_invalid + end + 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']) } + + it 'accepts info@example.com' do + user = build(:user, email: "info@example.com") + expect(user).to be_valid + end + + it 'rejects info@test.example.com' do + user = build(:user, email: "info@test.example.com") + expect(user).to be_invalid + end + + it 'rejects example@test.com' do + user = build(:user, email: "example@test.com") + expect(user).to be_invalid + end + end end end describe "Respond to" do - it { should respond_to(:is_admin?) } - it { should respond_to(:name) } - it { should respond_to(:private_token) } + it { is_expected.to respond_to(:is_admin?) } + it { is_expected.to respond_to(:name) } + it { is_expected.to respond_to(:private_token) } + end + + describe '#to_reference' do + let(:user) { create(:user) } + + it 'returns a String reference to the object' do + expect(user.to_reference).to eq "@#{user.username}" + end end describe '#generate_password' do it "should execute callback when force_random_password specified" do user = build(:user, force_random_password: true) - user.should_receive(:generate_password) + expect(user).to receive(:generate_password) user.save end it "should not generate password by default" do user = create(:user, password: 'abcdefghe') - user.password.should == 'abcdefghe' + expect(user.password).to eq('abcdefghe') end it "should generate password when forcing random password" do - Devise.stub(:friendly_token).and_return('123456789') + allow(Devise).to receive(:friendly_token).and_return('123456789') user = create(:user, password: 'abcdefg', force_random_password: true) - user.password.should == '12345678' + expect(user.password).to eq('12345678') + end + end + + describe '#two_factor_enabled' do + it 'returns two-factor authentication status' do + enabled = build_stubbed(:user, two_factor_enabled: true) + disabled = build_stubbed(:user) + + expect(enabled).to be_two_factor_enabled + expect(disabled).not_to be_two_factor_enabled + end + end + + describe '#two_factor_enabled=' do + it 'enables two-factor authentication' do + user = build_stubbed(:user, two_factor_enabled: false) + expect { user.two_factor_enabled = true }. + to change { user.two_factor_enabled? }.to(true) + end + + it 'disables two-factor authentication' do + user = build_stubbed(:user, two_factor_enabled: true) + expect { user.two_factor_enabled = false }. + to change { user.two_factor_enabled? }.to(false) end end describe 'authentication token' do it "should have authentication token" do user = create(:user) - user.authentication_token.should_not be_blank + expect(user.authentication_token).not_to be_blank end end @@ -152,15 +252,15 @@ describe User do @project_3.team << [@user, :developer] end - it { @user.authorized_projects.should include(@project) } - it { @user.authorized_projects.should include(@project_2) } - it { @user.authorized_projects.should include(@project_3) } - it { @user.owned_projects.should include(@project) } - it { @user.owned_projects.should_not include(@project_2) } - it { @user.owned_projects.should_not include(@project_3) } - it { @user.personal_projects.should include(@project) } - it { @user.personal_projects.should_not include(@project_2) } - it { @user.personal_projects.should_not include(@project_3) } + it { expect(@user.authorized_projects).to include(@project) } + it { expect(@user.authorized_projects).to include(@project_2) } + it { expect(@user.authorized_projects).to include(@project_3) } + it { expect(@user.owned_projects).to include(@project) } + it { expect(@user.owned_projects).not_to include(@project_2) } + it { expect(@user.owned_projects).not_to include(@project_3) } + it { expect(@user.personal_projects).to include(@project) } + it { expect(@user.personal_projects).not_to include(@project_2) } + it { expect(@user.personal_projects).not_to include(@project_3) } end describe 'groups' do @@ -170,9 +270,10 @@ describe User do @group.add_owner(@user) end - it { @user.several_namespaces?.should be_true } - it { @user.authorized_groups.should == [@group] } - it { @user.owned_groups.should == [@group] } + it { expect(@user.several_namespaces?).to be_truthy } + it { expect(@user.authorized_groups).to eq([@group]) } + it { expect(@user.owned_groups).to eq([@group]) } + it { expect(@user.namespaces).to match_array([@user.namespace, @group]) } end describe 'group multiple owners' do @@ -185,7 +286,7 @@ describe User do @group.add_user(@user2, GroupMember::OWNER) end - it { @user2.several_namespaces?.should be_true } + it { expect(@user2.several_namespaces?).to be_truthy } end describe 'namespaced' do @@ -194,7 +295,8 @@ describe User do @project = create :project, namespace: @user.namespace end - it { @user.several_namespaces?.should be_false } + it { expect(@user.several_namespaces?).to be_falsey } + it { expect(@user.namespaces).to eq([@user.namespace]) } end describe 'blocking user' do @@ -202,7 +304,7 @@ describe User do it "should block user" do user.block - user.blocked?.should be_true + expect(user.blocked?).to be_truthy end end @@ -214,10 +316,10 @@ describe User do @blocked = create :user, state: :blocked end - it { User.filter("admins").should == [@admin] } - it { User.filter("blocked").should == [@blocked] } - it { User.filter("wop").should include(@user, @admin, @blocked) } - it { User.filter(nil).should include(@user, @admin) } + it { expect(User.filter("admins")).to eq([@admin]) } + it { expect(User.filter("blocked")).to eq([@blocked]) } + it { expect(User.filter("wop")).to include(@user, @admin, @blocked) } + it { expect(User.filter(nil)).to include(@user, @admin) } end describe :not_in_project do @@ -227,52 +329,77 @@ describe User do @project = create :project end - it { User.not_in_project(@project).should include(@user, @project.owner) } + it { expect(User.not_in_project(@project)).to include(@user, @project.owner) } end describe 'user creation' do describe 'normal user' do let(:user) { create(:user, name: 'John Smith') } - it { user.is_admin?.should be_false } - it { user.require_ssh_key?.should be_true } - it { user.can_create_group?.should be_true } - it { user.can_create_project?.should be_true } - it { user.first_name.should == 'John' } + it { expect(user.is_admin?).to be_falsey } + it { expect(user.require_ssh_key?).to be_truthy } + it { expect(user.can_create_group?).to be_truthy } + it { expect(user.can_create_project?).to be_truthy } + it { expect(user.first_name).to eq('John') } end describe 'with defaults' do let(:user) { User.new } it "should apply defaults to user" do - user.projects_limit.should == Gitlab.config.gitlab.default_projects_limit - user.can_create_group.should == Gitlab.config.gitlab.default_can_create_group - user.theme_id.should == Gitlab.config.gitlab.default_theme + expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) + expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) + expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) end end describe 'with default overrides' do - let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: Gitlab::Theme::BASIC) } + let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: 1) } it "should apply defaults to user" do - user.projects_limit.should == 123 - user.can_create_group.should be_false - user.theme_id.should == Gitlab::Theme::BASIC + expect(user.projects_limit).to eq(123) + expect(user.can_create_group).to be_falsey + expect(user.theme_id).to eq(1) end end end + describe '.find_for_commit' do + it 'finds by primary email' do + user = create(:user, email: 'foo@example.com') + + expect(User.find_for_commit(user.email, '')).to eq user + end + + it 'finds by secondary email' do + email = create(:email, email: 'foo@example.com') + user = email.user + + expect(User.find_for_commit(email.email, '')).to eq user + end + + it 'finds by name' do + user = create(:user, name: 'Joey JoJo') + + expect(User.find_for_commit('', 'Joey JoJo')).to eq user + end + + it 'returns nil when nothing found' do + expect(User.find_for_commit('', '')).to be_nil + end + end + describe 'search' do let(:user1) { create(:user, username: 'James', email: 'james@testing.com') } let(:user2) { create(:user, username: 'jameson', email: 'jameson@example.com') } it "should be case insensitive" do - User.search(user1.username.upcase).to_a.should == [user1] - User.search(user1.username.downcase).to_a.should == [user1] - User.search(user2.username.upcase).to_a.should == [user2] - User.search(user2.username.downcase).to_a.should == [user2] - User.search(user1.username.downcase).to_a.count.should == 2 - User.search(user2.username.downcase).to_a.count.should == 1 + expect(User.search(user1.username.upcase).to_a).to eq([user1]) + expect(User.search(user1.username.downcase).to_a).to eq([user1]) + expect(User.search(user2.username.upcase).to_a).to eq([user2]) + expect(User.search(user2.username.downcase).to_a).to eq([user2]) + expect(User.search(user1.username.downcase).to_a.count).to eq(2) + expect(User.search(user2.username.downcase).to_a.count).to eq(1) end end @@ -280,10 +407,10 @@ describe User do let(:user1) { create(:user, username: 'foo') } it "should get the correct user" do - User.by_username_or_id(user1.id).should == user1 - User.by_username_or_id('foo').should == user1 - User.by_username_or_id(-1).should be_nil - User.by_username_or_id('bar').should be_nil + expect(User.by_username_or_id(user1.id)).to eq(user1) + expect(User.by_username_or_id('foo')).to eq(user1) + expect(User.by_username_or_id(-1)).to be_nil + expect(User.by_username_or_id('bar')).to be_nil end end @@ -302,13 +429,13 @@ describe User do end describe 'all_ssh_keys' do - it { should have_many(:keys).dependent(:destroy) } + it { is_expected.to have_many(:keys).dependent(:destroy) } it "should have all ssh keys" do user = create :user key = create :key, key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD33bWLBxu48Sev9Fert1yzEO4WGcWglWF7K/AwblIUFselOt/QdOL9DSjpQGxLagO1s9wl53STIO8qGS4Ms0EJZyIXOEFMjFJ5xmjSy+S37By4sG7SsltQEHMxtbtFOaW5LV2wCrX+rUsRNqLMamZjgjcPO0/EgGCXIGMAYW4O7cwGZdXWYIhQ1Vwy+CsVMDdPkPgBXqK7nR/ey8KMs8ho5fMNgB5hBw/AL9fNGhRw3QTD6Q12Nkhl4VZES2EsZqlpNnJttnPdp847DUsT6yuLRlfiQfz5Cn9ysHFdXObMN5VYIiPFwHeYCZp1X2S4fDZooRE8uOLTfxWHPXwrhqSH", user_id: user.id - user.all_ssh_keys.should include(key.key) + expect(user.all_ssh_keys).to include(key.key) end end @@ -317,12 +444,12 @@ describe User do it "should be true if avatar is image" do user.update_attribute(:avatar, 'uploads/avatar.png') - user.avatar_type.should be_true + expect(user.avatar_type).to be_truthy end it "should be false if avatar is html page" do user.update_attribute(:avatar, 'uploads/avatar.html') - user.avatar_type.should == ["only images allowed"] + expect(user.avatar_type).to eq(["only images allowed"]) end end @@ -331,51 +458,60 @@ describe User do it 'is false when LDAP is disabled' do # Create a condition which would otherwise cause 'true' to be returned - user.stub(ldap_user?: true) + allow(user).to receive(:ldap_user?).and_return(true) user.last_credential_check_at = nil - expect(user.requires_ldap_check?).to be_false + expect(user.requires_ldap_check?).to be_falsey end context 'when LDAP is enabled' do - before { Gitlab.config.ldap.stub(enabled: true) } + before do + allow(Gitlab.config.ldap).to receive(:enabled).and_return(true) + end it 'is false for non-LDAP users' do - user.stub(ldap_user?: false) - expect(user.requires_ldap_check?).to be_false + allow(user).to receive(:ldap_user?).and_return(false) + expect(user.requires_ldap_check?).to be_falsey end context 'and when the user is an LDAP user' do - before { user.stub(ldap_user?: true) } + before do + allow(user).to receive(:ldap_user?).and_return(true) + end it 'is true when the user has never had an LDAP check before' do user.last_credential_check_at = nil - expect(user.requires_ldap_check?).to be_true + expect(user.requires_ldap_check?).to be_truthy end it 'is true when the last LDAP check happened over 1 hour ago' do user.last_credential_check_at = 2.hours.ago - expect(user.requires_ldap_check?).to be_true + expect(user.requires_ldap_check?).to be_truthy end end end end describe :ldap_user? do - let(:user) { build(:user, :ldap) } - it "is true if provider name starts with ldap" do - user.provider = 'ldapmain' - expect( user.ldap_user? ).to be_true + user = create(:omniauth_user, provider: 'ldapmain') + expect( user.ldap_user? ).to be_truthy end it "is false for other providers" do - user.provider = 'other-provider' - expect( user.ldap_user? ).to be_false + user = create(:omniauth_user, provider: 'other-provider') + expect( user.ldap_user? ).to be_falsey end it "is false if no extern_uid is provided" do - user.extern_uid = nil - expect( user.ldap_user? ).to be_false + user = create(:omniauth_user, extern_uid: nil) + expect( user.ldap_user? ).to be_falsey + end + end + + describe :ldap_identity do + it "returns ldap identity" do + user = create :omniauth_user + expect(user.ldap_identity.provider).not_to be_empty end end @@ -429,24 +565,24 @@ describe User do project1 = create :project, :public project2 = create :project, :public - expect(user.starred?(project1)).to be_false - expect(user.starred?(project2)).to be_false + expect(user.starred?(project1)).to be_falsey + expect(user.starred?(project2)).to be_falsey star1 = UsersStarProject.create!(project: project1, user: user) - expect(user.starred?(project1)).to be_true - expect(user.starred?(project2)).to be_false + expect(user.starred?(project1)).to be_truthy + expect(user.starred?(project2)).to be_falsey star2 = UsersStarProject.create!(project: project2, user: user) - expect(user.starred?(project1)).to be_true - expect(user.starred?(project2)).to be_true + expect(user.starred?(project1)).to be_truthy + expect(user.starred?(project2)).to be_truthy star1.destroy - expect(user.starred?(project1)).to be_false - expect(user.starred?(project2)).to be_true + expect(user.starred?(project1)).to be_falsey + expect(user.starred?(project2)).to be_truthy star2.destroy - expect(user.starred?(project1)).to be_false - expect(user.starred?(project2)).to be_false + expect(user.starred?(project1)).to be_falsey + expect(user.starred?(project2)).to be_falsey end end @@ -455,11 +591,11 @@ describe User do user = create :user project = create :project, :public - expect(user.starred?(project)).to be_false + expect(user.starred?(project)).to be_falsey user.toggle_star(project) - expect(user.starred?(project)).to be_true + expect(user.starred?(project)).to be_truthy user.toggle_star(project) - expect(user.starred?(project)).to be_false + expect(user.starred?(project)).to be_falsey end end @@ -469,25 +605,69 @@ describe User do @user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha' @user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega' end - + it "sorts users as recently_signed_in" do - User.sort('recent_sign_in').first.should == @user + expect(User.sort('recent_sign_in').first).to eq(@user) end it "sorts users as late_signed_in" do - User.sort('oldest_sign_in').first.should == @user1 + expect(User.sort('oldest_sign_in').first).to eq(@user1) end it "sorts users as recently_created" do - User.sort('recently_created').first.should == @user + expect(User.sort('created_desc').first).to eq(@user) end it "sorts users as late_created" do - User.sort('late_created').first.should == @user1 + expect(User.sort('created_asc').first).to eq(@user1) end it "sorts users by name when nil is passed" do - User.sort(nil).first.should == @user + expect(User.sort(nil).first).to eq(@user) + end + end + + describe "#contributed_projects_ids" do + subject { create(:user) } + let!(:project1) { create(:project) } + let!(:project2) { create(:project, forked_from_project: project3) } + let!(:project3) { create(:project) } + let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) } + let!(:push_event) { create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject) } + let!(:merge_event) { create(:event, action: Event::CREATED, project: project3, target: merge_request, author: subject) } + + before do + project1.team << [subject, :master] + project2.team << [subject, :master] + end + + it "includes IDs for projects the user has pushed to" do + expect(subject.contributed_projects_ids).to include(project1.id) + end + + it "includes IDs for projects the user has had merge requests merged into" do + expect(subject.contributed_projects_ids).to include(project3.id) + end + + it "doesn't include IDs for unrelated projects" do + expect(subject.contributed_projects_ids).not_to include(project2.id) + end + end + + describe :can_be_removed? do + subject { create(:user) } + + context 'no owned groups' do + it { expect(subject.can_be_removed?).to be_truthy } + end + + context 'has owned groups' do + before do + group = create(:group) + group.add_owner(subject) + end + + it { expect(subject.can_be_removed?).to be_falsey } end end end diff --git a/spec/models/web_hook_spec.rb b/spec/models/web_hook_spec.rb deleted file mode 100644 index e9c04ee89cb..00000000000 --- a/spec/models/web_hook_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -# == Schema Information -# -# Table name: web_hooks -# -# id :integer not null, primary key -# url :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) 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) -# - -require 'spec_helper' - -describe ProjectHook do - describe "Associations" do - it { should belong_to :project } - end - - describe "Mass assignment" do - end - - describe "Validations" do - it { should validate_presence_of(:url) } - - context "url format" do - it { should allow_value("http://example.com").for(:url) } - it { should allow_value("https://excample.com").for(:url) } - it { should allow_value("http://test.com/api").for(:url) } - it { should allow_value("http://test.com/api?key=abc").for(:url) } - it { should allow_value("http://test.com/api?key=abc&type=def").for(:url) } - - it { should_not allow_value("example.com").for(:url) } - it { should_not allow_value("ftp://example.com").for(:url) } - it { should_not allow_value("herp-and-derp").for(:url) } - end - end - - describe "execute" do - before(:each) do - @project_hook = create(:project_hook) - @project = create(:project) - @project.hooks << [@project_hook] - @data = { before: 'oldrev', after: 'newrev', ref: 'ref'} - - WebMock.stub_request(:post, @project_hook.url) - end - - it "POSTs to the web hook URL" do - @project_hook.execute(@data) - WebMock.should have_requested(:post, @project_hook.url).once - end - - it "POSTs the data as JSON" do - json = @data.to_json - - @project_hook.execute(@data) - WebMock.should have_requested(:post, @project_hook.url).with(body: json).once - end - - it "catches exceptions" do - WebHook.should_receive(:post).and_raise("Some HTTP Post error") - - lambda { - @project_hook.execute(@data) - }.should raise_error - end - end -end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index d065431ee3a..fceb7668cac 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -16,27 +16,27 @@ describe WikiPage do end it "sets the slug attribute" do - @wiki_page.slug.should == "test-page" + expect(@wiki_page.slug).to eq("test-page") end it "sets the title attribute" do - @wiki_page.title.should == "test page" + expect(@wiki_page.title).to eq("test page") end it "sets the formatted content attribute" do - @wiki_page.content.should == "test content" + expect(@wiki_page.content).to eq("test content") end it "sets the format attribute" do - @wiki_page.format.should == :markdown + expect(@wiki_page.format).to eq(:markdown) end it "sets the message attribute" do - @wiki_page.message.should == "test commit" + expect(@wiki_page.message).to eq("test commit") end it "sets the version attribute" do - @wiki_page.version.should be_a Grit::Commit + expect(@wiki_page.version).to be_a Gollum::Git::Commit end end end @@ -48,12 +48,12 @@ describe WikiPage do it "validates presence of title" do subject.attributes.delete(:title) - subject.valid?.should be_false + expect(subject.valid?).to be_falsey end it "validates presence of content" do subject.attributes.delete(:content) - subject.valid?.should be_false + expect(subject.valid?).to be_falsey end end @@ -69,11 +69,52 @@ describe WikiPage do context "with valid attributes" do it "saves the wiki page" do subject.create(@wiki_attr) - wiki.find_page("Index").should_not be_nil + expect(wiki.find_page("Index")).not_to be_nil end it "returns true" do - subject.create(@wiki_attr).should == true + expect(subject.create(@wiki_attr)).to eq(true) + end + end + end + + describe "dot in the title" do + let(:title) { 'Index v1.2.3' } + + before do + @wiki_attr = {title: title, content: "Home Page", format: "markdown"} + end + + describe "#create" do + after do + destroy_page(title) + end + + context "with valid attributes" do + it "saves the wiki page" do + subject.create(@wiki_attr) + expect(wiki.find_page(title)).not_to be_nil + end + + it "returns true" do + expect(subject.create(@wiki_attr)).to eq(true) + end + end + end + + describe "#update" do + before do + create_page(title, "content") + @page = wiki.find_page(title) + end + + it "updates the content of the page" do + @page.update("new content") + @page = wiki.find_page(title) + end + + it "returns true" do + expect(@page.update("more content")).to be_truthy end end end @@ -95,7 +136,7 @@ describe WikiPage do end it "returns true" do - @page.update("more content").should be_true + expect(@page.update("more content")).to be_truthy end end end @@ -108,11 +149,11 @@ describe WikiPage do it "should delete the page" do @page.delete - wiki.pages.should be_empty + expect(wiki.pages).to be_empty end it "should return true" do - @page.delete.should == true + expect(@page.delete).to eq(true) end end @@ -128,7 +169,7 @@ describe WikiPage do it "returns an array of all commits for the page" do 3.times { |i| @page.update("content #{i}") } - @page.versions.count.should == 4 + expect(@page.versions.count).to eq(4) end end @@ -144,7 +185,7 @@ describe WikiPage do it "should be replace a hyphen to a space" do @page.title = "Import-existing-repositories-into-GitLab" - @page.title.should == "Import existing repositories into GitLab" + expect(@page.title).to eq("Import existing repositories into GitLab") end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 00000000000..671fd6c8666 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1 @@ +require "spec_helper" diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index e2f222c0d34..4048c297013 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -41,132 +41,133 @@ describe API, api: true do describe ".current_user" do it "should return nil for an invalid token" do env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = 'invalid token' - current_user.should be_nil + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil end it "should return nil for a user without access" do env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token - Gitlab::UserAccess.stub(allowed?: false) - current_user.should be_nil + allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + expect(current_user).to be_nil end it "should leave user as is when sudo not specified" do env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token - current_user.should == user + expect(current_user).to eq(user) clear_env params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = user.private_token - current_user.should == user + expect(current_user).to eq(user) end it "should change current user to sudo when admin" do set_env(admin, user.id) - current_user.should == user + expect(current_user).to eq(user) set_param(admin, user.id) - current_user.should == user + expect(current_user).to eq(user) set_env(admin, user.username) - current_user.should == user + expect(current_user).to eq(user) set_param(admin, user.username) - current_user.should == user + expect(current_user).to eq(user) end it "should throw an error when the current user is not an admin and attempting to sudo" do set_env(user, admin.id) - expect { current_user }.to raise_error + expect { current_user }.to raise_error(Exception) set_param(user, admin.id) - expect { current_user }.to raise_error + expect { current_user }.to raise_error(Exception) set_env(user, admin.username) - expect { current_user }.to raise_error + expect { current_user }.to raise_error(Exception) set_param(user, admin.username) - expect { current_user }.to raise_error + expect { current_user }.to raise_error(Exception) end it "should throw an error when the user cannot be found for a given id" do id = user.id + admin.id - user.id.should_not == id - admin.id.should_not == id + expect(user.id).not_to eq(id) + expect(admin.id).not_to eq(id) set_env(admin, id) - expect { current_user }.to raise_error + expect { current_user }.to raise_error(Exception) set_param(admin, id) - expect { current_user }.to raise_error + expect { current_user }.to raise_error(Exception) end it "should throw an error when the user cannot be found for a given username" do username = "#{user.username}#{admin.username}" - user.username.should_not == username - admin.username.should_not == username + expect(user.username).not_to eq(username) + expect(admin.username).not_to eq(username) set_env(admin, username) - expect { current_user }.to raise_error + expect { current_user }.to raise_error(Exception) set_param(admin, username) - expect { current_user }.to raise_error + expect { current_user }.to raise_error(Exception) end it "should handle sudo's to oneself" do set_env(admin, admin.id) - current_user.should == admin + expect(current_user).to eq(admin) set_param(admin, admin.id) - current_user.should == admin + expect(current_user).to eq(admin) set_env(admin, admin.username) - current_user.should == admin + expect(current_user).to eq(admin) set_param(admin, admin.username) - current_user.should == admin + expect(current_user).to eq(admin) end it "should handle multiple sudo's to oneself" do set_env(admin, user.id) - current_user.should == user - current_user.should == user + expect(current_user).to eq(user) + expect(current_user).to eq(user) set_env(admin, user.username) - current_user.should == user - current_user.should == user + expect(current_user).to eq(user) + expect(current_user).to eq(user) set_param(admin, user.id) - current_user.should == user - current_user.should == user + expect(current_user).to eq(user) + expect(current_user).to eq(user) set_param(admin, user.username) - current_user.should == user - current_user.should == user + expect(current_user).to eq(user) + expect(current_user).to eq(user) end it "should handle multiple sudo's to oneself using string ids" do set_env(admin, user.id.to_s) - current_user.should == user - current_user.should == user + expect(current_user).to eq(user) + expect(current_user).to eq(user) set_param(admin, user.id.to_s) - current_user.should == user - current_user.should == user + expect(current_user).to eq(user) + expect(current_user).to eq(user) end end describe '.sudo_identifier' do it "should return integers when input is an int" do set_env(admin, '123') - sudo_identifier.should == 123 + expect(sudo_identifier).to eq(123) set_env(admin, '0001234567890') - sudo_identifier.should == 1234567890 + expect(sudo_identifier).to eq(1234567890) set_param(admin, '123') - sudo_identifier.should == 123 + expect(sudo_identifier).to eq(123) set_param(admin, '0001234567890') - sudo_identifier.should == 1234567890 + expect(sudo_identifier).to eq(1234567890) end it "should return string when input is an is not an int" do set_env(admin, '12.30') - sudo_identifier.should == "12.30" + expect(sudo_identifier).to eq("12.30") set_env(admin, 'hello') - sudo_identifier.should == 'hello' + expect(sudo_identifier).to eq('hello') set_env(admin, ' 123') - sudo_identifier.should == ' 123' + expect(sudo_identifier).to eq(' 123') set_param(admin, '12.30') - sudo_identifier.should == "12.30" + expect(sudo_identifier).to eq("12.30") set_param(admin, 'hello') - sudo_identifier.should == 'hello' + expect(sudo_identifier).to eq('hello') set_param(admin, ' 123') - sudo_identifier.should == ' 123' + expect(sudo_identifier).to eq(' 123') end end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index b45572c39fd..cb6e5e89625 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -15,79 +15,79 @@ describe API::API, api: true do describe "GET /projects/:id/repository/branches" do it "should return an array of project branches" do get api("/projects/#{project.id}/repository/branches", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['name'].should == project.repository.branch_names.first + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project.repository.branch_names.first) end end describe "GET /projects/:id/repository/branches/:branch" do it "should return the branch information for a single branch" do get api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response['name'].should == branch_name - json_response['commit']['id'].should == branch_sha - json_response['protected'].should == false + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(false) end it "should return a 403 error if guest" do get api("/projects/#{project.id}/repository/branches", user2) - response.status.should == 403 + expect(response.status).to eq(403) end it "should return a 404 error if branch is not available" do get api("/projects/#{project.id}/repository/branches/unknown", user) - response.status.should == 404 + expect(response.status).to eq(404) end end describe "PUT /projects/:id/repository/branches/:branch/protect" do it "should protect a single branch" do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response['name'].should == branch_name - json_response['commit']['id'].should == branch_sha - json_response['protected'].should == true + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) end it "should return a 404 error if branch not found" do put api("/projects/#{project.id}/repository/branches/unknown/protect", user) - response.status.should == 404 + expect(response.status).to eq(404) end it "should return a 403 error if guest" do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2) - response.status.should == 403 + expect(response.status).to eq(403) end it "should return success when protect branch again" do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - response.status.should == 200 + expect(response.status).to eq(200) end end describe "PUT /projects/:id/repository/branches/:branch/unprotect" do it "should unprotect a single branch" do put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response['name'].should == branch_name - json_response['commit']['id'].should == branch_sha - json_response['protected'].should == false + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(false) end it "should return success when unprotect branch" do put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user) - response.status.should == 404 + expect(response.status).to eq(404) end it "should return success when unprotect branch again" do put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user) put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user) - response.status.should == 200 + expect(response.status).to eq(200) end end @@ -97,74 +97,76 @@ describe API::API, api: true do branch_name: 'feature1', ref: branch_sha - response.status.should == 201 + expect(response.status).to eq(201) - json_response['name'].should == 'feature1' - json_response['commit']['id'].should == branch_sha + expect(json_response['name']).to eq('feature1') + expect(json_response['commit']['id']).to eq(branch_sha) end it "should deny for user without push access" do post api("/projects/#{project.id}/repository/branches", user2), branch_name: branch_name, ref: branch_sha - response.status.should == 403 + expect(response.status).to eq(403) end it 'should return 400 if branch name is invalid' do post api("/projects/#{project.id}/repository/branches", user), branch_name: 'new design', ref: branch_sha - response.status.should == 400 - json_response['message'].should == 'Branch name invalid' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Branch name invalid') end it 'should return 400 if branch already exists' do post api("/projects/#{project.id}/repository/branches", user), branch_name: 'new_design1', ref: branch_sha - response.status.should == 201 + expect(response.status).to eq(201) post api("/projects/#{project.id}/repository/branches", user), branch_name: 'new_design1', ref: branch_sha - response.status.should == 400 - json_response['message'].should == 'Branch already exists' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Branch already exists') end it 'should return 400 if ref name is invalid' do post api("/projects/#{project.id}/repository/branches", user), branch_name: 'new_design3', ref: 'foo' - response.status.should == 400 - json_response['message'].should == 'Invalid reference name' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Invalid reference name') end end describe "DELETE /projects/:id/repository/branches/:branch" do - before { Repository.any_instance.stub(rm_branch: true) } + before do + allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) + end it "should remove branch" do delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - response.status.should == 200 - json_response['branch_name'].should == branch_name + expect(response.status).to eq(200) + expect(json_response['branch_name']).to eq(branch_name) end it 'should return 404 if branch not exists' do delete api("/projects/#{project.id}/repository/branches/foobar", user) - response.status.should == 404 + expect(response.status).to eq(404) end it "should remove protected branch" do project.protected_branches.create(name: branch_name) delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - response.status.should == 405 - json_response['message'].should == 'Protected branch cant be removed' + expect(response.status).to eq(405) + expect(json_response['message']).to eq('Protected branch cant be removed') end it "should not remove HEAD branch" do delete api("/projects/#{project.id}/repository/branches/master", user) - response.status.should == 405 - json_response['message'].should == 'Cannot remove HEAD branch' + expect(response.status).to eq(405) + expect(json_response['message']).to eq('Cannot remove HEAD branch') end end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index a3f58f50913..a1c248c636e 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -9,6 +9,7 @@ describe API::API, api: true do let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } + let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') } before { project.team << [user, :reporter] } @@ -18,17 +19,17 @@ describe API::API, api: true do it "should return project commits" do get api("/projects/#{project.id}/repository/commits", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response.should be_an Array - json_response.first['id'].should == project.repository.commit.id + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(project.repository.commit.id) end end context "unauthorized user" do it "should not return project commits" do get api("/projects/#{project.id}/repository/commits") - response.status.should == 401 + expect(response.status).to eq(401) end end end @@ -37,21 +38,21 @@ describe API::API, api: true do context "authorized user" do it "should return a commit by sha" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) - response.status.should == 200 - json_response['id'].should == project.repository.commit.id - json_response['title'].should == project.repository.commit.title + expect(response.status).to eq(200) + expect(json_response['id']).to eq(project.repository.commit.id) + expect(json_response['title']).to eq(project.repository.commit.title) end it "should return a 404 error if not found" do get api("/projects/#{project.id}/repository/commits/invalid_sha", user) - response.status.should == 404 + expect(response.status).to eq(404) end end context "unauthorized user" do it "should not return the selected commit" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}") - response.status.should == 401 + expect(response.status).to eq(401) end end end @@ -62,23 +63,23 @@ describe API::API, api: true do it "should return the diff of the selected commit" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response.should be_an Array - json_response.length.should >= 1 - json_response.first.keys.should include "diff" + expect(json_response).to be_an Array + expect(json_response.length).to be >= 1 + expect(json_response.first.keys).to include "diff" end it "should return a 404 error if invalid commit" do get api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user) - response.status.should == 404 + expect(response.status).to eq(404) end end context "unauthorized user" do it "should not return the diff of the selected commit" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff") - response.status.should == 401 + expect(response.status).to eq(401) end end end @@ -87,23 +88,23 @@ describe API::API, api: true do context 'authorized user' do it 'should return merge_request comments' do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['note'].should == 'a comment on a commit' - json_response.first['author']['id'].should == user.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['note']).to eq('a comment on a commit') + expect(json_response.first['author']['id']).to eq(user.id) end it 'should return a 404 error if merge_request_id not found' do get api("/projects/#{project.id}/repository/commits/1234ab/comments", user) - response.status.should == 404 + expect(response.status).to eq(404) end end context 'unauthorized user' do it 'should not return the diff of the selected commit' do get api("/projects/#{project.id}/repository/commits/1234ab/comments") - response.status.should == 401 + expect(response.status).to eq(401) end end end @@ -112,37 +113,37 @@ describe API::API, api: true do context 'authorized user' do it 'should return comment' do post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment' - response.status.should == 201 - json_response['note'].should == 'My comment' - json_response['path'].should be_nil - json_response['line'].should be_nil - json_response['line_type'].should be_nil + expect(response.status).to eq(201) + expect(json_response['note']).to eq('My comment') + expect(json_response['path']).to be_nil + expect(json_response['line']).to be_nil + expect(json_response['line_type']).to be_nil end it 'should return the inline comment' do post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.diffs.first.new_path, line: 7, line_type: 'new' - response.status.should == 201 - json_response['note'].should == 'My comment' - json_response['path'].should == project.repository.commit.diffs.first.new_path - json_response['line'].should == 7 - json_response['line_type'].should == 'new' + expect(response.status).to eq(201) + expect(json_response['note']).to eq('My comment') + expect(json_response['path']).to eq(project.repository.commit.diffs.first.new_path) + expect(json_response['line']).to eq(7) + expect(json_response['line_type']).to eq('new') end it 'should return 400 if note is missing' do post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return 404 if note is attached to non existent commit' do post api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment' - response.status.should == 404 + expect(response.status).to eq(404) end end context 'unauthorized user' do it 'should not return the diff of the selected commit' do post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments") - response.status.should == 401 + expect(response.status).to eq(401) end end end diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb new file mode 100644 index 00000000000..39949a90422 --- /dev/null +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let!(:user) { create(:user) } + let!(:application) { Doorkeeper::Application.create!(:name => "MyApp", :redirect_uri => "https://app.com", :owner => user) } + let!(:token) { Doorkeeper::AccessToken.create! :application_id => application.id, :resource_owner_id => user.id } + + + describe "when unauthenticated" do + it "returns authentication success" do + get api("/user"), :access_token => token.token + expect(response.status).to eq(200) + end + end + + describe "when token invalid" do + it "returns authentication error" do + get api("/user"), :access_token => "123a" + expect(response.status).to eq(401) + end + end + + describe "authorization by private token" do + it "returns authentication success" do + get api("/user", user) + expect(response.status).to eq(200) + end + end +end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index b43a202aec0..8a6b4b8a170 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -16,15 +16,15 @@ describe API::API, api: true do } get api("/projects/#{project.id}/repository/files", user), params - response.status.should == 200 - json_response['file_path'].should == file_path - json_response['file_name'].should == 'popen.rb' - Base64.decode64(json_response['content']).lines.first.should == "require 'fileutils'\n" + expect(response.status).to eq(200) + expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_name']).to eq('popen.rb') + expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") end it "should return a 400 bad request if no params given" do get api("/projects/#{project.id}/repository/files", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 404 if such file does not exist" do @@ -34,7 +34,7 @@ describe API::API, api: true do } get api("/projects/#{project.id}/repository/files", user), params - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -49,27 +49,22 @@ describe API::API, api: true do } it "should create a new file in project repo" do - Gitlab::Satellite::NewFileAction.any_instance.stub( - commit!: true, - ) - post api("/projects/#{project.id}/repository/files", user), valid_params - response.status.should == 201 - json_response['file_path'].should == 'newfile.rb' + expect(response.status).to eq(201) + expect(json_response['file_path']).to eq('newfile.rb') end it "should return a 400 bad request if no params given" do post api("/projects/#{project.id}/repository/files", user) - response.status.should == 400 + expect(response.status).to eq(400) end - it "should return a 400 if satellite fails to create file" do - Gitlab::Satellite::NewFileAction.any_instance.stub( - commit!: false, - ) + it "should return a 400 if editor fails to create file" do + allow_any_instance_of(Repository).to receive(:commit_file). + and_return(false) post api("/projects/#{project.id}/repository/files", user), valid_params - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -84,27 +79,14 @@ describe API::API, api: true do } it "should update existing file in project repo" do - Gitlab::Satellite::EditFileAction.any_instance.stub( - commit!: true, - ) - put api("/projects/#{project.id}/repository/files", user), valid_params - response.status.should == 200 - json_response['file_path'].should == file_path + expect(response.status).to eq(200) + expect(json_response['file_path']).to eq(file_path) end it "should return a 400 bad request if no params given" do put api("/projects/#{project.id}/repository/files", user) - response.status.should == 400 - end - - it "should return a 400 if satellite fails to create file" do - Gitlab::Satellite::EditFileAction.any_instance.stub( - commit!: false, - ) - - put api("/projects/#{project.id}/repository/files", user), valid_params - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -118,27 +100,21 @@ describe API::API, api: true do } it "should delete existing file in project repo" do - Gitlab::Satellite::DeleteFileAction.any_instance.stub( - commit!: true, - ) - delete api("/projects/#{project.id}/repository/files", user), valid_params - response.status.should == 200 - json_response['file_path'].should == file_path + expect(response.status).to eq(200) + expect(json_response['file_path']).to eq(file_path) end it "should return a 400 bad request if no params given" do delete api("/projects/#{project.id}/repository/files", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 400 if satellite fails to create file" do - Gitlab::Satellite::DeleteFileAction.any_instance.stub( - commit!: false, - ) + allow_any_instance_of(Repository).to receive(:remove_file).and_return(false) delete api("/projects/#{project.id}/repository/files", user), valid_params - response.status.should == 400 + expect(response.status).to eq(400) end end end diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb index cbbd1e7de5a..7a784796031 100644 --- a/spec/requests/api/fork_spec.rb +++ b/spec/requests/api/fork_spec.rb @@ -23,50 +23,49 @@ describe API::API, api: true do context 'when authenticated' do it 'should fork if user has sufficient access to project' do post api("/projects/fork/#{project.id}", user2) - response.status.should == 201 - json_response['name'].should == project.name - json_response['path'].should == project.path - json_response['owner']['id'].should == user2.id - json_response['namespace']['id'].should == user2.namespace.id - json_response['forked_from_project']['id'].should == project.id + expect(response.status).to eq(201) + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to eq(project.path) + expect(json_response['owner']['id']).to eq(user2.id) + expect(json_response['namespace']['id']).to eq(user2.namespace.id) + expect(json_response['forked_from_project']['id']).to eq(project.id) end it 'should fork if user is admin' do post api("/projects/fork/#{project.id}", admin) - response.status.should == 201 - json_response['name'].should == project.name - json_response['path'].should == project.path - json_response['owner']['id'].should == admin.id - json_response['namespace']['id'].should == admin.namespace.id - json_response['forked_from_project']['id'].should == project.id + expect(response.status).to eq(201) + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to eq(project.path) + expect(json_response['owner']['id']).to eq(admin.id) + expect(json_response['namespace']['id']).to eq(admin.namespace.id) + expect(json_response['forked_from_project']['id']).to eq(project.id) end it 'should fail on missing project access for the project to fork' do post api("/projects/fork/#{project.id}", user3) - response.status.should == 404 - json_response['message'].should == '404 Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Project Not Found') end it 'should fail if forked project exists in the user namespace' do post api("/projects/fork/#{project.id}", user) - response.status.should == 409 - json_response['message']['base'].should == ['Invalid fork destination'] - json_response['message']['name'].should == ['has already been taken'] - json_response['message']['path'].should == ['has already been taken'] + expect(response.status).to eq(409) + expect(json_response['message']['name']).to eq(['has already been taken']) + expect(json_response['message']['path']).to eq(['has already been taken']) end it 'should fail if project to fork from does not exist' do post api('/projects/fork/424242', user) - response.status.should == 404 - json_response['message'].should == '404 Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Project Not Found') end end context 'when unauthenticated' do it 'should return authentication error' do post api("/projects/fork/#{project.id}") - response.status.should == 401 - json_response['message'].should == '401 Unauthorized' + expect(response.status).to eq(401) + expect(json_response['message']).to eq('401 Unauthorized') end end end diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb index 4957186f605..8ba6876a95b 100644 --- a/spec/requests/api/group_members_spec.rb +++ b/spec/requests/api/group_members_spec.rb @@ -31,20 +31,20 @@ describe API::API, api: true do it "each user: should return an array of members groups of group3" do [owner, master, developer, reporter, guest].each do |user| get api("/groups/#{group_with_members.id}/members", user) - response.status.should == 200 - json_response.should be_an Array - json_response.size.should == 5 - json_response.find { |e| e['id']==owner.id }['access_level'].should == GroupMember::OWNER - json_response.find { |e| e['id']==reporter.id }['access_level'].should == GroupMember::REPORTER - json_response.find { |e| e['id']==developer.id }['access_level'].should == GroupMember::DEVELOPER - json_response.find { |e| e['id']==master.id }['access_level'].should == GroupMember::MASTER - json_response.find { |e| e['id']==guest.id }['access_level'].should == GroupMember::GUEST + 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) end end it "users not part of the group should get access error" do get api("/groups/#{group_with_members.id}/members", stranger) - response.status.should == 403 + expect(response.status).to eq(403) end end end @@ -53,7 +53,7 @@ describe API::API, api: true do context "when not a member of the group" do it "should not add guest as member of group_no_members when adding being done by person outside the group" do post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER - response.status.should == 403 + expect(response.status).to eq(403) end end @@ -66,9 +66,9 @@ describe API::API, api: true do user_id: new_user.id, access_level: GroupMember::MASTER }.to change { group_no_members.members.count }.by(1) - response.status.should == 201 - json_response['name'].should == new_user.name - json_response['access_level'].should == GroupMember::MASTER + expect(response.status).to eq(201) + expect(json_response['name']).to eq(new_user.name) + expect(json_response['access_level']).to eq(GroupMember::MASTER) end it "should not allow guest to modify group members" do @@ -79,27 +79,90 @@ describe API::API, api: true do user_id: new_user.id, access_level: GroupMember::MASTER }.not_to change { group_with_members.members.count } - response.status.should == 403 + expect(response.status).to eq(403) end it "should return error if member already exists" do post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER - response.status.should == 409 + expect(response.status).to eq(409) end it "should return a 400 error when user id is not given" do post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 400 error when access level is not given" do post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 422 error when access level is not known" do post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234 - response.status.should == 422 + expect(response.status).to eq(422) + end + end + end + + describe 'PUT /groups/:id/members/:user_id' do + context 'when not a member of the group' do + it 'should return a 409 error if the user is not a group member' do + put( + api("/groups/#{group_no_members.id}/members/#{developer.id}", + owner), access_level: GroupMember::MASTER + ) + expect(response.status).to eq(404) + end + end + + context 'when a member of the group' do + it 'should return ok and update member access level' do + put( + api("/groups/#{group_with_members.id}/members/#{reporter.id}", + owner), + access_level: GroupMember::MASTER + ) + + expect(response.status).to eq(200) + + get api("/groups/#{group_with_members.id}/members", owner) + json_reporter = json_response.find do |e| + e['id'] == reporter.id + end + + expect(json_reporter['access_level']).to eq(GroupMember::MASTER) + end + + it 'should not allow guest to modify group members' do + put( + api("/groups/#{group_with_members.id}/members/#{developer.id}", + guest), + access_level: GroupMember::MASTER + ) + + expect(response.status).to eq(403) + + get api("/groups/#{group_with_members.id}/members", owner) + json_developer = json_response.find do |e| + e['id'] == developer.id + end + + expect(json_developer['access_level']).to eq(GroupMember::DEVELOPER) + end + + it 'should return a 400 error when access level is not given' do + put( + api("/groups/#{group_with_members.id}/members/#{master.id}", owner) + ) + expect(response.status).to eq(400) + end + + it 'should return a 422 error when access level is not known' do + put( + api("/groups/#{group_with_members.id}/members/#{master.id}", owner), + access_level: 1234 + ) + expect(response.status).to eq(422) end end end @@ -109,7 +172,7 @@ describe API::API, api: true do it "should not delete guest's membership of group_with_members" do random_user = create(:user) delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user) - response.status.should == 403 + expect(response.status).to eq(403) end end @@ -119,17 +182,17 @@ describe API::API, api: true do delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner) }.to change { group_with_members.members.count }.by(-1) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return a 404 error when user id is not known" do delete api("/groups/#{group_with_members.id}/members/1328", owner) - response.status.should == 404 + expect(response.status).to eq(404) end it "should not allow guest to modify group members" do delete api("/groups/#{group_with_members.id}/members/#{master.id}", guest) - response.status.should == 403 + expect(response.status).to eq(403) end end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 8dfd2cd650e..56aa97adcc3 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers - let(:user1) { create(:user) } + let(:user1) { create(:user, can_create_group: false) } let(:user2) { create(:user) } + let(:user3) { create(:user) } let(:admin) { create(:admin) } let!(:group1) { create(:group) } let!(:group2) { create(:group) } @@ -18,26 +19,26 @@ describe API::API, api: true do context "when unauthenticated" do it "should return authentication error" do get api("/groups") - response.status.should == 401 + expect(response.status).to eq(401) end end context "when authenticated as user" do it "normal user: should return an array of groups of user1" do get api("/groups", user1) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['name'].should == group1.name + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(group1.name) end end context "when authenticated as admin" do it "admin: should return an array of all groups" do get api("/groups", admin) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 2 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) end end end @@ -46,62 +47,81 @@ describe API::API, api: true do context "when authenticated as user" do it "should return one of user1's groups" do get api("/groups/#{group1.id}", user1) - response.status.should == 200 + expect(response.status).to eq(200) json_response['name'] == group1.name end it "should not return a non existing group" do get api("/groups/1328", user1) - response.status.should == 404 + expect(response.status).to eq(404) end it "should not return a group not attached to user1" do get api("/groups/#{group2.id}", user1) - response.status.should == 403 + expect(response.status).to eq(403) end end context "when authenticated as admin" do it "should return any existing group" do get api("/groups/#{group2.id}", admin) - response.status.should == 200 + expect(response.status).to eq(200) json_response['name'] == group2.name end it "should not return a non existing group" do get api("/groups/1328", admin) - response.status.should == 404 + expect(response.status).to eq(404) + end + end + + context 'when using group path in URL' do + it 'should return any existing group' do + get api("/groups/#{group1.path}", admin) + expect(response.status).to eq(200) + json_response['name'] == group2.name + end + + it 'should not return a non existing group' do + get api('/groups/unknown', admin) + expect(response.status).to eq(404) + end + + it 'should not return a group not attached to user1' do + get api("/groups/#{group2.path}", user1) + expect(response.status).to eq(403) end end end describe "POST /groups" do - context "when authenticated as user" do + context "when authenticated as user without group permissions" do it "should not create group" do post api("/groups", user1), attributes_for(:group) - response.status.should == 403 + expect(response.status).to eq(403) end end - context "when authenticated as admin" do + context "when authenticated as user with group permissions" do it "should create group" do - post api("/groups", admin), attributes_for(:group) - response.status.should == 201 + post api("/groups", user3), attributes_for(:group) + expect(response.status).to eq(201) end it "should not create group, duplicate" do - post api("/groups", admin), {name: "Duplicate Test", path: group2.path} - response.status.should == 404 + post api("/groups", user3), {name: 'Duplicate Test', path: group2.path} + expect(response.status).to eq(400) + expect(response.message).to eq("Bad Request") end it "should return 400 bad request error if name not given" do - post api("/groups", admin), {path: group2.path} - response.status.should == 400 + post api("/groups", user3), {path: group2.path} + expect(response.status).to eq(400) end it "should return 400 bad request error if path not given" do - post api("/groups", admin), { name: 'test' } - response.status.should == 400 + post api("/groups", user3), {name: 'test'} + expect(response.status).to eq(400) end end end @@ -110,36 +130,36 @@ describe API::API, api: true do context "when authenticated as user" do it "should remove group" do delete api("/groups/#{group1.id}", user1) - response.status.should == 200 + expect(response.status).to eq(200) end it "should not remove a group if not an owner" do - user3 = create(:user) - group1.add_user(user3, Gitlab::Access::MASTER) + user4 = create(:user) + group1.add_user(user4, Gitlab::Access::MASTER) delete api("/groups/#{group1.id}", user3) - response.status.should == 403 + expect(response.status).to eq(403) end it "should not remove a non existing group" do delete api("/groups/1328", user1) - response.status.should == 404 + expect(response.status).to eq(404) end it "should not remove a group not attached to user1" do delete api("/groups/#{group2.id}", user1) - response.status.should == 403 + expect(response.status).to eq(403) end end context "when authenticated as admin" do it "should remove any existing group" do delete api("/groups/#{group2.id}", admin) - response.status.should == 200 + expect(response.status).to eq(200) end it "should not remove a non existing group" do delete api("/groups/1328", admin) - response.status.should == 404 + expect(response.status).to eq(404) end end end @@ -147,21 +167,22 @@ describe API::API, api: true do describe "POST /groups/:id/projects/:project_id" do let(:project) { create(:project) } before(:each) do - Projects::TransferService.any_instance.stub(execute: true) - Project.stub(:find).and_return(project) + allow_any_instance_of(Projects::TransferService). + to receive(:execute).and_return(true) + allow(Project).to receive(:find).and_return(project) end context "when authenticated as user" do it "should not transfer project to group" do post api("/groups/#{group1.id}/projects/#{project.id}", user2) - response.status.should == 403 + expect(response.status).to eq(403) end end context "when authenticated as admin" do it "should transfer project to group" do post api("/groups/#{group1.id}/projects/#{project.id}", admin) - response.status.should == 201 + expect(response.status).to eq(201) end end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 4faa1f9b964..8d0ae1475c2 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -5,14 +5,36 @@ describe API::API, api: true do let(:user) { create(:user) } let(:key) { create(:key, user: user) } let(:project) { create(:project) } - let(:secret_token) { File.read Rails.root.join('.gitlab_shell_secret') } + let(:secret_token) { File.read Gitlab.config.gitlab_shell.secret_file } describe "GET /internal/check", no_db: true do it do get api("/internal/check"), secret_token: secret_token - response.status.should == 200 - json_response['api_version'].should == API::API.version + expect(response.status).to eq(200) + expect(json_response['api_version']).to eq(API::API.version) + end + end + + describe "GET /internal/broadcast_message" do + context "broadcast message exists" do + let!(:broadcast_message) { create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow ) } + + it do + get api("/internal/broadcast_message"), secret_token: secret_token + + expect(response.status).to eq(200) + expect(json_response["message"]).to eq(broadcast_message.message) + end + end + + context "broadcast message doesn't exist" do + it do + get api("/internal/broadcast_message"), secret_token: secret_token + + expect(response.status).to eq(200) + expect(json_response).to be_empty + end end end @@ -20,9 +42,9 @@ describe API::API, api: true do it do get(api("/internal/discover"), key_id: key.id, secret_token: secret_token) - response.status.should == 200 + expect(response.status).to eq(200) - json_response['name'].should == user.name + expect(json_response['name']).to eq(user.name) end end @@ -36,8 +58,8 @@ describe API::API, api: true do it do pull(key, project) - response.status.should == 200 - JSON.parse(response.body)["status"].should be_true + expect(response.status).to eq(200) + expect(json_response["status"]).to be_truthy end end @@ -45,8 +67,8 @@ describe API::API, api: true do it do push(key, project) - response.status.should == 200 - JSON.parse(response.body)["status"].should be_true + expect(response.status).to eq(200) + expect(json_response["status"]).to be_truthy end end end @@ -60,8 +82,8 @@ describe API::API, api: true do it do pull(key, project) - response.status.should == 200 - JSON.parse(response.body)["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end @@ -69,8 +91,8 @@ describe API::API, api: true do it do push(key, project) - response.status.should == 200 - JSON.parse(response.body)["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end end @@ -86,8 +108,8 @@ describe API::API, api: true do it do pull(key, personal_project) - response.status.should == 200 - JSON.parse(response.body)["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end @@ -95,8 +117,8 @@ describe API::API, api: true do it do push(key, personal_project) - response.status.should == 200 - JSON.parse(response.body)["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end end @@ -113,8 +135,8 @@ describe API::API, api: true do it do pull(key, project) - response.status.should == 200 - JSON.parse(response.body)["status"].should be_true + expect(response.status).to eq(200) + expect(json_response["status"]).to be_truthy end end @@ -122,8 +144,8 @@ describe API::API, api: true do it do push(key, project) - response.status.should == 200 - JSON.parse(response.body)["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end end @@ -139,8 +161,8 @@ describe API::API, api: true do it do archive(key, project) - response.status.should == 200 - JSON.parse(response.body)["status"].should be_true + expect(response.status).to eq(200) + expect(json_response["status"]).to be_truthy end end @@ -148,8 +170,8 @@ describe API::API, api: true do it do archive(key, project) - response.status.should == 200 - JSON.parse(response.body)["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end end @@ -158,8 +180,8 @@ describe API::API, api: true do it do pull(key, OpenStruct.new(path_with_namespace: 'gitlab/notexists')) - response.status.should == 200 - JSON.parse(response.body)["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end @@ -167,8 +189,8 @@ describe API::API, api: true do it do pull(OpenStruct.new(id: 0), project) - response.status.should == 200 - JSON.parse(response.body)["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 775d7b4e18d..5e65ad18c0e 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -34,86 +34,87 @@ describe API::API, api: true do context "when unauthenticated" do it "should return authentication error" do get api("/issues") - response.status.should == 401 + expect(response.status).to eq(401) end end context "when authenticated" do it "should return an array of issues" do get api("/issues", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['title'].should == issue.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(issue.title) end it "should add pagination headers" do get api("/issues?per_page=3", user) - response.headers['Link'].should == + expect(response.headers['Link']).to eq( '<http://www.example.com/api/v3/issues?page=1&per_page=3>; rel="first", <http://www.example.com/api/v3/issues?page=1&per_page=3>; rel="last"' + ) end it 'should return an array of closed issues' do get api('/issues?state=closed', user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['id'].should == closed_issue.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) end it 'should return an array of opened issues' do get api('/issues?state=opened', user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['id'].should == issue.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(issue.id) end it 'should return an array of all issues' do get api('/issues?state=all', user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 2 - json_response.first['id'].should == issue.id - json_response.second['id'].should == closed_issue.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) end it 'should return an array of labeled issues' do get api("/issues?labels=#{label.title}", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['labels'].should == [label.title] + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) end it 'should return an array of labeled issues when at least one label matches' do get api("/issues?labels=#{label.title},foo,bar", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['labels'].should == [label.title] + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) end it 'should return an empty array if no issue matches labels' do get api('/issues?labels=foo,bar', user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 0 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end it 'should return an array of labeled issues matching given state' do get api("/issues?labels=#{label.title}&state=opened", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['labels'].should == [label.title] - json_response.first['state'].should == 'opened' + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) + expect(json_response.first['state']).to eq('opened') end it 'should return an empty array if no issue matches labels and state filters' do get api("/issues?labels=#{label.title}&state=closed", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 0 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end end end @@ -124,78 +125,86 @@ describe API::API, api: true do it "should return project issues" do get api("#{base_url}/issues", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['title'].should == issue.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(issue.title) end it 'should return an array of labeled project issues' do get api("#{base_url}/issues?labels=#{label.title}", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['labels'].should == [label.title] + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) end it 'should return an array of labeled project issues when at least one label matches' do get api("#{base_url}/issues?labels=#{label.title},foo,bar", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['labels'].should == [label.title] + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) end it 'should return an empty array if no project issue matches labels' do get api("#{base_url}/issues?labels=foo,bar", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 0 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end it 'should return an empty array if no issue matches milestone' do get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 0 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end it 'should return an empty array if milestone does not exist' do get api("#{base_url}/issues?milestone=foo", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 0 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end it 'should return an array of issues in given milestone' do get api("#{base_url}/issues?milestone=#{title}", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 2 - json_response.first['id'].should == issue.id - json_response.second['id'].should == closed_issue.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) end it 'should return an array of issues matching state in milestone' do get api("#{base_url}/issues?milestone=#{milestone.title}"\ '&state=closed', user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['id'].should == closed_issue.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) end end describe "GET /projects/:id/issues/:issue_id" do it "should return a project issue by id" do get api("/projects/#{project.id}/issues/#{issue.id}", user) - response.status.should == 200 - json_response['title'].should == issue.title - json_response['iid'].should == issue.iid + expect(response.status).to eq(200) + expect(json_response['title']).to eq(issue.title) + expect(json_response['iid']).to eq(issue.iid) + end + + it 'should return a project issue by iid' do + get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user) + expect(response.status).to eq 200 + expect(json_response.first['title']).to eq issue.title + expect(json_response.first['id']).to eq issue.id + expect(json_response.first['iid']).to eq issue.iid end it "should return 404 if issue id not found" do get api("/projects/#{project.id}/issues/54321", user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -203,32 +212,32 @@ describe API::API, api: true do it "should create a new project issue" do post api("/projects/#{project.id}/issues", user), title: 'new issue', labels: 'label, label2' - response.status.should == 201 - json_response['title'].should == 'new issue' - json_response['description'].should be_nil - json_response['labels'].should == ['label', 'label2'] + expect(response.status).to eq(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['description']).to be_nil + expect(json_response['labels']).to eq(['label', 'label2']) end it "should return a 400 bad request if title not given" do post api("/projects/#{project.id}/issues", user), labels: 'label, label2' - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return 400 on invalid label names' do post api("/projects/#{project.id}/issues", user), title: 'new issue', labels: 'label, ?' - response.status.should == 400 - json_response['message']['labels']['?']['title'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) end it 'should return 400 if title is too long' do post api("/projects/#{project.id}/issues", user), title: 'g' * 256 - response.status.should == 400 - json_response['message']['title'].should == [ + expect(response.status).to eq(400) + expect(json_response['message']['title']).to eq([ 'is too long (maximum is 255 characters)' - ] + ]) end end @@ -236,23 +245,23 @@ describe API::API, api: true do it "should update a project issue" do put api("/projects/#{project.id}/issues/#{issue.id}", user), title: 'updated title' - response.status.should == 200 + expect(response.status).to eq(200) - json_response['title'].should == 'updated title' + expect(json_response['title']).to eq('updated title') end it "should return 404 error if issue id not found" do put api("/projects/#{project.id}/issues/44444", user), title: 'updated title' - response.status.should == 404 + expect(response.status).to eq(404) end it 'should return 400 on invalid label names' do put api("/projects/#{project.id}/issues/#{issue.id}", user), title: 'updated title', labels: 'label, ?' - response.status.should == 400 - json_response['message']['labels']['?']['title'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) end end @@ -263,49 +272,49 @@ describe API::API, api: true do it 'should not update labels if not present' do put api("/projects/#{project.id}/issues/#{issue.id}", user), title: 'updated title' - response.status.should == 200 - json_response['labels'].should == [label.title] + expect(response.status).to eq(200) + expect(json_response['labels']).to eq([label.title]) end it 'should remove all labels' do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' - response.status.should == 200 - json_response['labels'].should == [] + expect(response.status).to eq(200) + expect(json_response['labels']).to eq([]) end it 'should update labels' do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: 'foo,bar' - response.status.should == 200 - json_response['labels'].should include 'foo' - json_response['labels'].should include 'bar' + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'foo' + expect(json_response['labels']).to include 'bar' end it 'should return 400 on invalid label names' do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: 'label, ?' - response.status.should == 400 - json_response['message']['labels']['?']['title'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) end it 'should allow special label names' do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: 'label:foo, label-bar,label_bar,label/bar' - response.status.should == 200 - json_response['labels'].should include 'label:foo' - json_response['labels'].should include 'label-bar' - json_response['labels'].should include 'label_bar' - json_response['labels'].should include 'label/bar' + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'label:foo' + expect(json_response['labels']).to include 'label-bar' + expect(json_response['labels']).to include 'label_bar' + expect(json_response['labels']).to include 'label/bar' end it 'should return 400 if title is too long' do put api("/projects/#{project.id}/issues/#{issue.id}", user), title: 'g' * 256 - response.status.should == 400 - json_response['message']['title'].should == [ + expect(response.status).to eq(400) + expect(json_response['message']['title']).to eq([ 'is too long (maximum is 255 characters)' - ] + ]) end end @@ -313,17 +322,17 @@ describe API::API, api: true do it "should update a project issue" do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: 'label2', state_event: "close" - response.status.should == 200 + expect(response.status).to eq(200) - json_response['labels'].should include 'label2' - json_response['state'].should eq "closed" + expect(json_response['labels']).to include 'label2' + expect(json_response['state']).to eq "closed" end end describe "DELETE /projects/:id/issues/:issue_id" do it "should delete a project issue" do delete api("/projects/#{project.id}/issues/#{issue.id}", user) - response.status.should == 405 + expect(response.status).to eq(405) end end end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index dbddc8a7da4..aff109a9424 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -15,10 +15,10 @@ describe API::API, api: true do describe 'GET /projects/:id/labels' do it 'should return project labels' do get api("/projects/#{project.id}/labels", user) - response.status.should == 200 - json_response.should be_an Array - json_response.size.should == 1 - json_response.first['name'].should == label1.name + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['name']).to eq(label1.name) end end @@ -27,69 +27,69 @@ describe API::API, api: true do post api("/projects/#{project.id}/labels", user), name: 'Foo', color: '#FFAABB' - response.status.should == 201 - json_response['name'].should == 'Foo' - json_response['color'].should == '#FFAABB' + expect(response.status).to eq(201) + expect(json_response['name']).to eq('Foo') + expect(json_response['color']).to eq('#FFAABB') end it 'should return a 400 bad request if name not given' do post api("/projects/#{project.id}/labels", user), color: '#FFAABB' - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return a 400 bad request if color not given' do post api("/projects/#{project.id}/labels", user), name: 'Foobar' - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return 400 for invalid color' do post api("/projects/#{project.id}/labels", user), name: 'Foo', color: '#FFAA' - response.status.should == 400 - json_response['message']['color'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['color']).to eq(['is invalid']) end it 'should return 400 for too long color code' do post api("/projects/#{project.id}/labels", user), name: 'Foo', color: '#FFAAFFFF' - response.status.should == 400 - json_response['message']['color'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['color']).to eq(['is invalid']) end it 'should return 400 for invalid name' do post api("/projects/#{project.id}/labels", user), name: '?', color: '#FFAABB' - response.status.should == 400 - json_response['message']['title'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['title']).to eq(['is invalid']) end it 'should return 409 if label already exists' do post api("/projects/#{project.id}/labels", user), name: 'label1', color: '#FFAABB' - response.status.should == 409 - json_response['message'].should == 'Label already exists' + expect(response.status).to eq(409) + expect(json_response['message']).to eq('Label already exists') end end describe 'DELETE /projects/:id/labels' do it 'should return 200 for existing label' do delete api("/projects/#{project.id}/labels", user), name: 'label1' - response.status.should == 200 + expect(response.status).to eq(200) end it 'should return 404 for non existing label' do delete api("/projects/#{project.id}/labels", user), name: 'label2' - response.status.should == 404 - json_response['message'].should == '404 Label Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Label Not Found') end it 'should return 400 for wrong parameters' do delete api("/projects/#{project.id}/labels", user) - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -99,47 +99,47 @@ describe API::API, api: true do name: 'label1', new_name: 'New Label', color: '#FFFFFF' - response.status.should == 200 - json_response['name'].should == 'New Label' - json_response['color'].should == '#FFFFFF' + expect(response.status).to eq(200) + expect(json_response['name']).to eq('New Label') + expect(json_response['color']).to eq('#FFFFFF') end it 'should return 200 if name is changed' do put api("/projects/#{project.id}/labels", user), name: 'label1', new_name: 'New Label' - response.status.should == 200 - json_response['name'].should == 'New Label' - json_response['color'].should == label1.color + expect(response.status).to eq(200) + expect(json_response['name']).to eq('New Label') + expect(json_response['color']).to eq(label1.color) end it 'should return 200 if colors is changed' do put api("/projects/#{project.id}/labels", user), name: 'label1', color: '#FFFFFF' - response.status.should == 200 - json_response['name'].should == label1.name - json_response['color'].should == '#FFFFFF' + expect(response.status).to eq(200) + expect(json_response['name']).to eq(label1.name) + expect(json_response['color']).to eq('#FFFFFF') end it 'should return 404 if label does not exist' do put api("/projects/#{project.id}/labels", user), name: 'label2', new_name: 'label3' - response.status.should == 404 + expect(response.status).to eq(404) end it 'should return 400 if no label name given' do put api("/projects/#{project.id}/labels", user), new_name: 'label2' - response.status.should == 400 - json_response['message'].should == '400 (Bad request) "name" not given' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "name" not given') end it 'should return 400 if no new parameters given' do put api("/projects/#{project.id}/labels", user), name: 'label1' - response.status.should == 400 - json_response['message'].should == 'Required parameters '\ - '"new_name" or "color" missing' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Required parameters '\ + '"new_name" or "color" missing') end it 'should return 400 for invalid name' do @@ -147,24 +147,24 @@ describe API::API, api: true do name: 'label1', new_name: '?', color: '#FFFFFF' - response.status.should == 400 - json_response['message']['title'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['title']).to eq(['is invalid']) end it 'should return 400 for invalid name' do put api("/projects/#{project.id}/labels", user), name: 'label1', color: '#FF' - response.status.should == 400 - json_response['message']['color'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['color']).to eq(['is invalid']) end it 'should return 400 for too long color code' do post api("/projects/#{project.id}/labels", user), name: 'Foo', color: '#FFAAFFFF' - response.status.should == 400 - json_response['message']['color'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['color']).to eq(['is invalid']) end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 5ba3a330991..70dd8049b12 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -16,46 +16,50 @@ describe API::API, api: true do context "when unauthenticated" do it "should return authentication error" do get api("/projects/#{project.id}/merge_requests") - response.status.should == 401 + expect(response.status).to eq(401) end end context "when authenticated" do it "should return an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 3 - json_response.first['title'].should == merge_request.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['title']).to eq(merge_request.title) end + it "should return an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests?state", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 3 - json_response.first['title'].should == merge_request.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['title']).to eq(merge_request.title) end + it "should return an array of open merge_requests" do get api("/projects/#{project.id}/merge_requests?state=opened", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['title'].should == merge_request.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.last['title']).to eq(merge_request.title) end + it "should return an array of closed merge_requests" do get api("/projects/#{project.id}/merge_requests?state=closed", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 2 - json_response.first['title'].should == merge_request_closed.title - json_response.second['title'].should == merge_request_merged.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.second['title']).to eq(merge_request_closed.title) + expect(json_response.first['title']).to eq(merge_request_merged.title) end + it "should return an array of merged merge_requests" do get api("/projects/#{project.id}/merge_requests?state=merged", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['title'].should == merge_request_merged.title + 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(merge_request_merged.title) end context "with ordering" do @@ -66,35 +70,38 @@ describe API::API, api: true do it "should return an array of merge_requests in ascending order" do get api("/projects/#{project.id}/merge_requests?sort=asc", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 3 - json_response.first['id'].should == @mr_earlier.id - json_response.last['id'].should == @mr_later.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['id']).to eq(@mr_earlier.id) + expect(json_response.first['id']).to eq(@mr_later.id) end + it "should return an array of merge_requests in descending order" do get api("/projects/#{project.id}/merge_requests?sort=desc", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 3 - json_response.first['id'].should == @mr_later.id - json_response.last['id'].should == @mr_earlier.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['id']).to eq(@mr_later.id) + expect(json_response.last['id']).to eq(@mr_earlier.id) end + it "should return an array of merge_requests ordered by updated_at" do get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 3 - json_response.first['id'].should == @mr_earlier.id - json_response.last['id'].should == @mr_later.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['id']).to eq(@mr_earlier.id) + expect(json_response.first['id']).to eq(@mr_later.id) end + it "should return an array of merge_requests ordered by created_at" do get api("/projects/#{project.id}/merge_requests?sort=created_at", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 3 - json_response.first['id'].should == @mr_earlier.id - json_response.last['id'].should == @mr_later.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['id']).to eq(@mr_earlier.id) + expect(json_response.first['id']).to eq(@mr_later.id) end end end @@ -103,14 +110,35 @@ describe API::API, api: true do describe "GET /projects/:id/merge_request/:merge_request_id" do it "should return merge_request" do get api("/projects/#{project.id}/merge_request/#{merge_request.id}", user) - response.status.should == 200 - json_response['title'].should == merge_request.title - json_response['iid'].should == merge_request.iid + expect(response.status).to eq(200) + expect(json_response['title']).to eq(merge_request.title) + expect(json_response['iid']).to eq(merge_request.iid) + end + + it 'should return merge_request by iid' do + url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}" + get api(url, user) + expect(response.status).to eq 200 + expect(json_response.first['title']).to eq merge_request.title + expect(json_response.first['id']).to eq merge_request.id end it "should return a 404 error if merge_request_id not found" do get api("/projects/#{project.id}/merge_request/999", user) - response.status.should == 404 + expect(response.status).to eq(404) + end + end + + describe 'GET /projects/:id/merge_request/:merge_request_id/changes' do + it 'should return the change information of the merge_request' do + get api("/projects/#{project.id}/merge_request/#{merge_request.id}/changes", user) + expect(response.status).to eq 200 + expect(json_response['changes'].size).to eq(merge_request.diffs.size) + end + + it 'returns a 404 when merge_request_id not found' do + get api("/projects/#{project.id}/merge_request/999/changes", user) + expect(response.status).to eq(404) end end @@ -123,33 +151,33 @@ describe API::API, api: true do target_branch: 'master', author: user, labels: 'label, label2' - response.status.should == 201 - json_response['title'].should == 'Test merge_request' - json_response['labels'].should == ['label', 'label2'] + expect(response.status).to eq(201) + expect(json_response['title']).to eq('Test merge_request') + expect(json_response['labels']).to eq(['label', 'label2']) end it "should return 422 when source_branch equals target_branch" do post api("/projects/#{project.id}/merge_requests", user), title: "Test merge_request", source_branch: "master", target_branch: "master", author: user - response.status.should == 422 + expect(response.status).to eq(422) end it "should return 400 when source_branch is missing" do post api("/projects/#{project.id}/merge_requests", user), title: "Test merge_request", target_branch: "master", author: user - response.status.should == 400 + expect(response.status).to eq(400) end it "should return 400 when target_branch is missing" do post api("/projects/#{project.id}/merge_requests", user), title: "Test merge_request", source_branch: "stable", author: user - response.status.should == 400 + expect(response.status).to eq(400) end it "should return 400 when title is missing" do post api("/projects/#{project.id}/merge_requests", user), target_branch: 'master', source_branch: 'stable' - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return 400 on invalid label names' do @@ -159,9 +187,10 @@ describe API::API, api: true do target_branch: 'master', author: user, labels: 'label, ?' - response.status.should == 400 - json_response['message']['labels']['?']['title'].should == + expect(response.status).to eq(400) + expect(json_response['message']['labels']['?']['title']).to eq( ['is invalid'] + ) end context 'with existing MR' do @@ -182,7 +211,7 @@ describe API::API, api: true do target_branch: 'master', author: user end.to change { MergeRequest.count }.by(0) - response.status.should == 409 + expect(response.status).to eq(409) end end end @@ -199,37 +228,37 @@ describe API::API, api: true do it "should return merge_request" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "stable", target_branch: "master", author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' - response.status.should == 201 - json_response['title'].should == 'Test merge_request' - json_response['description'].should == 'Test description for Test merge_request' + expect(response.status).to eq(201) + expect(json_response['title']).to eq('Test merge_request') + expect(json_response['description']).to eq('Test description for Test merge_request') end it "should not return 422 when source_branch equals target_branch" do - project.id.should_not == fork_project.id - fork_project.forked?.should be_true - fork_project.forked_from_project.should == project + expect(project.id).not_to eq(fork_project.id) + expect(fork_project.forked?).to be_truthy + expect(fork_project.forked_from_project).to eq(project) post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id - response.status.should == 201 - json_response['title'].should == 'Test merge_request' + expect(response.status).to eq(201) + expect(json_response['title']).to eq('Test merge_request') end it "should return 400 when source_branch is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - response.status.should == 400 + expect(response.status).to eq(400) end it "should return 400 when target_branch is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - response.status.should == 400 + expect(response.status).to eq(400) end it "should return 400 when title is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), target_branch: 'master', source_branch: 'stable', author: user2, target_project_id: project.id - response.status.should == 400 + expect(response.status).to eq(400) end context 'when target_branch is specified' do @@ -240,7 +269,7 @@ describe API::API, api: true do source_branch: 'stable', author: user, target_project_id: fork_project.id - response.status.should == 422 + expect(response.status).to eq(422) end it 'should return 422 if targeting a different fork' do @@ -250,14 +279,14 @@ describe API::API, api: true do source_branch: 'stable', author: user2, target_project_id: unrelated_project.id - response.status.should == 422 + expect(response.status).to eq(422) end end it "should return 201 when target_branch is specified and for the same project" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: 'master', source_branch: 'stable', author: user2, target_project_id: fork_project.id - response.status.should == 201 + expect(response.status).to eq(201) end end end @@ -265,64 +294,77 @@ describe API::API, api: true do describe "PUT /projects/:id/merge_request/:merge_request_id to close MR" do it "should return merge_request" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), state_event: "close" - response.status.should == 200 - json_response['state'].should == 'closed' + expect(response.status).to eq(200) + expect(json_response['state']).to eq('closed') end end describe "PUT /projects/:id/merge_request/:merge_request_id/merge" do it "should return merge_request in case of success" do - MergeRequest.any_instance.stub(can_be_merged?: true, automerge!: true) + allow_any_instance_of(MergeRequest). + to receive_messages(can_be_merged?: true, automerge!: true) + put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) - response.status.should == 200 + + expect(response.status).to eq(200) end it "should return 405 if branch can't be merged" do - MergeRequest.any_instance.stub(can_be_merged?: false) + allow_any_instance_of(MergeRequest). + to receive(:can_be_merged?).and_return(false) + put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) - response.status.should == 405 - json_response['message'].should == 'Branch cannot be merged' + + expect(response.status).to eq(405) + expect(json_response['message']).to eq('Branch cannot be merged') end it "should return 405 if merge_request is not open" do merge_request.close put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) - response.status.should == 405 - json_response['message'].should == '405 Method Not Allowed' + expect(response.status).to eq(405) + expect(json_response['message']).to eq('405 Method Not Allowed') + end + + it "should return 405 if merge_request is a work in progress" do + merge_request.update_attribute(:title, "WIP: #{merge_request.title}") + put api("/projects/#{project.id}/merge_request/#{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] put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user2) - response.status.should == 401 - json_response['message'].should == '401 Unauthorized' + expect(response.status).to eq(401) + expect(json_response['message']).to eq('401 Unauthorized') end end describe "PUT /projects/:id/merge_request/:merge_request_id" do it "should return merge_request" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), title: "New title" - response.status.should == 200 - json_response['title'].should == 'New title' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('New title') end it "should return merge_request" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), description: "New description" - response.status.should == 200 - json_response['description'].should == 'New description' + expect(response.status).to eq(200) + expect(json_response['description']).to eq('New description') end - it "should return 422 when source_branch and target_branch are renamed the same" do + it "should return 400 when source_branch is specified" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), source_branch: "master", target_branch: "master" - response.status.should == 422 + expect(response.status).to eq(400) end it "should return merge_request with renamed target_branch" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), target_branch: "wiki" - response.status.should == 200 - json_response['target_branch'].should == 'wiki' + expect(response.status).to eq(200) + expect(json_response['target_branch']).to eq('wiki') end it 'should return 400 on invalid label names' do @@ -330,43 +372,43 @@ describe API::API, api: true do user), title: 'new issue', labels: 'label, ?' - response.status.should == 400 - json_response['message']['labels']['?']['title'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) end end describe "POST /projects/:id/merge_request/:merge_request_id/comments" do it "should return comment" do post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user), note: "My comment" - response.status.should == 201 - json_response['note'].should == 'My comment' + expect(response.status).to eq(201) + expect(json_response['note']).to eq('My comment') end it "should return 400 if note is missing" do post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return 404 if note is attached to non existent merge request" do post api("/projects/#{project.id}/merge_request/404/comments", user), note: 'My comment' - response.status.should == 404 + expect(response.status).to eq(404) end end describe "GET :id/merge_request/:merge_request_id/comments" do it "should return merge_request comments" do get api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['note'].should == "a comment on a MR" - json_response.first['author']['id'].should == user.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['note']).to eq("a comment on a MR") + expect(json_response.first['author']['id']).to eq(user.id) end it "should return a 404 error if merge_request_id not found" do get api("/projects/#{project.id}/merge_request/999/comments", user) - response.status.should == 404 + expect(response.status).to eq(404) end end diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index f0619a1c801..db0f6e3c0f5 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -8,92 +8,116 @@ describe API::API, api: true do before { project.team << [user, :developer] } - describe "GET /projects/:id/milestones" do - it "should return project milestones" do + describe 'GET /projects/:id/milestones' do + it 'should return project milestones' do get api("/projects/#{project.id}/milestones", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['title'].should == milestone.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(milestone.title) end - it "should return a 401 error if user not authenticated" do + it 'should return a 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones") - response.status.should == 401 + expect(response.status).to eq(401) end end - describe "GET /projects/:id/milestones/:milestone_id" do - it "should return a project milestone by id" do + describe 'GET /projects/:id/milestones/:milestone_id' do + it 'should return a project milestone by id' do get api("/projects/#{project.id}/milestones/#{milestone.id}", user) - response.status.should == 200 - json_response['title'].should == milestone.title - json_response['iid'].should == milestone.iid + expect(response.status).to eq(200) + expect(json_response['title']).to eq(milestone.title) + expect(json_response['iid']).to eq(milestone.iid) end - it "should return 401 error if user not authenticated" do + it 'should return a project milestone by iid' do + get api("/projects/#{project.id}/milestones?iid=#{milestone.iid}", user) + expect(response.status).to eq 200 + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + + it 'should return 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones/#{milestone.id}") - response.status.should == 401 + expect(response.status).to eq(401) end - it "should return a 404 error if milestone id not found" do + it 'should return a 404 error if milestone id not found' do get api("/projects/#{project.id}/milestones/1234", user) - response.status.should == 404 + expect(response.status).to eq(404) end end - describe "POST /projects/:id/milestones" do - it "should create a new project milestone" do + describe 'POST /projects/:id/milestones' do + it 'should create a new project milestone' do post api("/projects/#{project.id}/milestones", user), title: 'new milestone' - response.status.should == 201 - json_response['title'].should == 'new milestone' - json_response['description'].should be_nil + expect(response.status).to eq(201) + expect(json_response['title']).to eq('new milestone') + expect(json_response['description']).to be_nil end - it "should create a new project milestone with description and due date" do + it 'should create a new project milestone with description and due date' do post api("/projects/#{project.id}/milestones", user), title: 'new milestone', description: 'release', due_date: '2013-03-02' - response.status.should == 201 - json_response['description'].should == 'release' - json_response['due_date'].should == '2013-03-02' + expect(response.status).to eq(201) + expect(json_response['description']).to eq('release') + expect(json_response['due_date']).to eq('2013-03-02') end - it "should return a 400 error if title is missing" do + it 'should return a 400 error if title is missing' do post api("/projects/#{project.id}/milestones", user) - response.status.should == 400 + expect(response.status).to eq(400) end end - describe "PUT /projects/:id/milestones/:milestone_id" do - it "should update a project milestone" do + describe 'PUT /projects/:id/milestones/:milestone_id' do + it 'should update a project milestone' do put api("/projects/#{project.id}/milestones/#{milestone.id}", user), title: 'updated title' - response.status.should == 200 - json_response['title'].should == 'updated title' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('updated title') end - it "should return a 404 error if milestone id not found" do + it 'should return a 404 error if milestone id not found' do put api("/projects/#{project.id}/milestones/1234", user), title: 'updated title' - response.status.should == 404 + expect(response.status).to eq(404) end end - describe "PUT /projects/:id/milestones/:milestone_id to close milestone" do - it "should update a project milestone" do + describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do + it 'should update a project milestone' do put api("/projects/#{project.id}/milestones/#{milestone.id}", user), state_event: 'close' - response.status.should == 200 + expect(response.status).to eq(200) - json_response['state'].should == 'closed' + expect(json_response['state']).to eq('closed') end end - describe "PUT /projects/:id/milestones/:milestone_id to test observer on close" do - it "should create an activity event when an milestone is closed" do - Event.should_receive(:create) + describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do + it 'should create an activity event when an milestone is closed' do + expect(Event).to receive(:create) put api("/projects/#{project.id}/milestones/#{milestone.id}", user), state_event: 'close' end end + + describe 'GET /projects/:id/milestones/:milestone_id/issues' do + before do + milestone.issues << create(:issue) + end + it 'should return project issues for a particular milestone' do + get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['milestone']['title']).to eq(milestone.title) + end + + it 'should return a 401 error if user not authenticated' do + get api("/projects/#{project.id}/milestones/#{milestone.id}/issues") + expect(response.status).to eq(401) + end + end end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index b8943ea0762..21787fdd895 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:admin) { create(:admin) } + let(:user) { create(:user) } let!(:group1) { create(:group) } let!(:group2) { create(:group) } @@ -10,17 +11,43 @@ describe API::API, api: true do context "when unauthenticated" do it "should return authentication error" do get api("/namespaces") - response.status.should == 401 + expect(response.status).to eq(401) end end - context "when authenticated as admin" do + context "when authenticated as admin" do it "admin: should return an array of all namespaces" do get api("/namespaces", admin) - response.status.should == 200 - json_response.should be_an Array + expect(response.status).to eq(200) + expect(json_response).to be_an Array - json_response.length.should == Namespace.count + expect(json_response.length).to eq(Namespace.count) + end + + it "admin: should return an array of matched namespaces" do + get api("/namespaces?search=#{group1.name}", admin) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + + expect(json_response.length).to eq(1) + end + end + + context "when authenticated as a regular user" do + it "user: should return an array of namespaces" do + get api("/namespaces", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + + expect(json_response.length).to eq(1) + end + + it "admin: should return an array of matched namespaces" do + get api("/namespaces?search=#{user.username}", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + + expect(json_response.length).to eq(1) end end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 7aa53787aed..8b177af4689 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -16,42 +16,42 @@ 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) - response.status.should == 200 - json_response.should be_an Array - json_response.first['body'].should == issue_note.note + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(issue_note.note) end it "should return a 404 error when issue id not found" do get api("/projects/#{project.id}/issues/123/notes", user) - response.status.should == 404 + expect(response.status).to eq(404) end end 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) - response.status.should == 200 - json_response.should be_an Array - json_response.first['body'].should == snippet_note.note + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(snippet_note.note) end it "should return a 404 error when snippet id not found" do get api("/projects/#{project.id}/snippets/42/notes", user) - response.status.should == 404 + expect(response.status).to eq(404) end end 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) - response.status.should == 200 - json_response.should be_an Array - json_response.first['body'].should == merge_request_note.note + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(merge_request_note.note) end it "should return a 404 error if merge request id not found" do get api("/projects/#{project.id}/merge_requests/4444/notes", user) - response.status.should == 404 + expect(response.status).to eq(404) end end end @@ -60,26 +60,26 @@ 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) - response.status.should == 200 - json_response['body'].should == issue_note.note + 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/123", user) - response.status.should == 404 + expect(response.status).to eq(404) end end 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) - response.status.should == 200 - json_response['body'].should == snippet_note.note + 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/123", user) - response.status.should == 404 + expect(response.status).to eq(404) end end end @@ -88,47 +88,101 @@ 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!' - response.status.should == 201 - json_response['body'].should == 'hi!' - json_response['author']['username'].should == user.username + expect(response.status).to eq(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) end it "should return a 400 bad request error if body not given" do post api("/projects/#{project.id}/issues/#{issue.id}/notes", user) - response.status.should == 400 + 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!' - response.status.should == 401 + expect(response.status).to eq(401) end end 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!' - response.status.should == 201 - json_response['body'].should == 'hi!' - json_response['author']['username'].should == user.username + expect(response.status).to eq(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) end it "should return a 400 bad request error if body not given" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) - response.status.should == 400 + 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!' - response.status.should == 401 + expect(response.status).to eq(401) end end end describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do it "should create an activity event when an issue note is created" do - Event.should_receive(:create) + expect(Event).to receive(:create) post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' end end + + describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do + context 'when noteable is an Issue' 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 + + it 'should return a 404 error when note id not found' do + put api("/projects/#{project.id}/issues/#{issue.id}/notes/123", 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 + + context 'when noteable is a Snippet' 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 + + it 'should return a 404 error when note id not found' do + put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/123", user), body: "Hello!" + expect(response.status).to eq(404) + end + end + + context 'when noteable is a Merge Request' 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 + + it 'should return a 404 error when note id not found' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ + "notes/123", user), body: "Hello!" + expect(response.status).to eq(404) + end + end + end + end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index cdb5e3d0612..81fe68de662 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -16,18 +16,18 @@ describe API::API, 'ProjectHooks', api: true do context "authorized user" do it "should return project hooks" do get api("/projects/#{project.id}/hooks", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response.should be_an Array - json_response.count.should == 1 - json_response.first['url'].should == "http://example.com" + expect(json_response).to be_an Array + expect(json_response.count).to eq(1) + expect(json_response.first['url']).to eq("http://example.com") end end context "unauthorized user" do it "should not access project hooks" do get api("/projects/#{project.id}/hooks", user3) - response.status.should == 403 + expect(response.status).to eq(403) end end end @@ -36,26 +36,26 @@ describe API::API, 'ProjectHooks', api: true do context "authorized user" do it "should return a project hook" do get api("/projects/#{project.id}/hooks/#{hook.id}", user) - response.status.should == 200 - json_response['url'].should == hook.url + expect(response.status).to eq(200) + expect(json_response['url']).to eq(hook.url) end it "should return a 404 error if hook id is not available" do get api("/projects/#{project.id}/hooks/1234", user) - response.status.should == 404 + expect(response.status).to eq(404) end end context "unauthorized user" do it "should not access an existing hook" do get api("/projects/#{project.id}/hooks/#{hook.id}", user3) - response.status.should == 403 + expect(response.status).to eq(403) end end it "should return a 404 error if hook id is not available" do get api("/projects/#{project.id}/hooks/1234", user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -65,17 +65,17 @@ describe API::API, 'ProjectHooks', api: true do post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true }.to change {project.hooks.count}.by(1) - response.status.should == 201 + expect(response.status).to eq(201) end it "should return a 400 error if url not given" do post api("/projects/#{project.id}/hooks", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 422 error if url not valid" do post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com" - response.status.should == 422 + expect(response.status).to eq(422) end end @@ -83,23 +83,23 @@ describe API::API, 'ProjectHooks', api: true do it "should update an existing project hook" do put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'http://example.org', push_events: false - response.status.should == 200 - json_response['url'].should == 'http://example.org' + expect(response.status).to eq(200) + expect(json_response['url']).to eq('http://example.org') end it "should return 404 error if hook id not found" do put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' - response.status.should == 404 + expect(response.status).to eq(404) end it "should return 400 error if url is not given" do put api("/projects/#{project.id}/hooks/#{hook.id}", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 422 error if url is not valid" do put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com' - response.status.should == 422 + expect(response.status).to eq(422) end end @@ -108,22 +108,22 @@ describe API::API, 'ProjectHooks', api: true do expect { delete api("/projects/#{project.id}/hooks/#{hook.id}", user) }.to change {project.hooks.count}.by(-1) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return success when deleting hook" do delete api("/projects/#{project.id}/hooks/#{hook.id}", user) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return success when deleting non existent hook" do delete api("/projects/#{project.id}/hooks/42", user) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return a 405 error if hook id not given" do delete api("/projects/#{project.id}/hooks", user) - response.status.should == 405 + expect(response.status).to eq(405) end end end diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb index 836f21f3e0b..4aeaa02f958 100644 --- a/spec/requests/api/project_members_spec.rb +++ b/spec/requests/api/project_members_spec.rb @@ -15,23 +15,23 @@ describe API::API, api: true do it "should return project team members" do get api("/projects/#{project.id}/members", user) - response.status.should == 200 - json_response.should be_an Array - json_response.count.should == 2 - json_response.map { |u| u['username'] }.should include user.username + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.count).to eq(2) + expect(json_response.map { |u| u['username'] }).to include user.username end it "finds team members with query string" do get api("/projects/#{project.id}/members", user), query: user.username - response.status.should == 200 - json_response.should be_an Array - json_response.count.should == 1 - json_response.first['username'].should == user.username + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.count).to eq(1) + expect(json_response.first['username']).to eq(user.username) end it "should return a 404 error if id not found" do get api("/projects/9999/members", user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -40,14 +40,14 @@ describe API::API, api: true do it "should return project team member" do get api("/projects/#{project.id}/members/#{user.id}", user) - response.status.should == 200 - json_response['username'].should == user.username - json_response['access_level'].should == ProjectMember::MASTER + expect(response.status).to eq(200) + expect(json_response['username']).to eq(user.username) + expect(json_response['access_level']).to eq(ProjectMember::MASTER) end it "should return a 404 error if user id not found" do get api("/projects/#{project.id}/members/1234", user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -58,9 +58,9 @@ describe API::API, api: true do access_level: ProjectMember::DEVELOPER }.to change { ProjectMember.count }.by(1) - response.status.should == 201 - json_response['username'].should == user2.username - json_response['access_level'].should == ProjectMember::DEVELOPER + expect(response.status).to eq(201) + expect(json_response['username']).to eq(user2.username) + expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER) end it "should return a 201 status if user is already project member" do @@ -69,26 +69,26 @@ describe API::API, api: true do expect { post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER - }.not_to change { ProjectMember.count }.by(1) + }.not_to change { ProjectMember.count } - response.status.should == 201 - json_response['username'].should == user2.username - json_response['access_level'].should == ProjectMember::DEVELOPER + expect(response.status).to eq(201) + expect(json_response['username']).to eq(user2.username) + expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER) end it "should return a 400 error when user id is not given" do post api("/projects/#{project.id}/members", user), access_level: ProjectMember::MASTER - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 400 error when access level is not given" do post api("/projects/#{project.id}/members", user), user_id: user2.id - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 422 error when access level is not known" do post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234 - response.status.should == 422 + expect(response.status).to eq(422) end end @@ -97,24 +97,24 @@ describe API::API, api: true do it "should update project team member" do put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: ProjectMember::MASTER - response.status.should == 200 - json_response['username'].should == user3.username - json_response['access_level'].should == ProjectMember::MASTER + expect(response.status).to eq(200) + expect(json_response['username']).to eq(user3.username) + expect(json_response['access_level']).to eq(ProjectMember::MASTER) end it "should return a 404 error if user_id is not found" do put api("/projects/#{project.id}/members/1234", user), access_level: ProjectMember::MASTER - response.status.should == 404 + expect(response.status).to eq(404) end it "should return a 400 error when access level is not given" do put api("/projects/#{project.id}/members/#{user3.id}", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 422 error when access level is not known" do put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123 - response.status.should == 422 + expect(response.status).to eq(422) end end @@ -132,22 +132,22 @@ describe API::API, api: true do delete api("/projects/#{project.id}/members/#{user3.id}", user) expect { delete api("/projects/#{project.id}/members/#{user3.id}", user) - }.to_not change { ProjectMember.count }.by(1) + }.not_to change { ProjectMember.count } end it "should return 200 if team member already removed" do delete api("/projects/#{project.id}/members/#{user3.id}", user) delete api("/projects/#{project.id}/members/#{user3.id}", user) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return 200 OK when the user was not member" do expect { delete api("/projects/#{project.id}/members/1000000", user) }.to change { ProjectMember.count }.by(0) - response.status.should == 200 - json_response['message'].should == "Access revoked" - json_response['id'].should == 1000000 + expect(response.status).to eq(200) + expect(json_response['message']).to eq("Access revoked") + expect(json_response['id']).to eq(1000000) end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 2c4b68c10b6..1386c03cb21 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1,220 +1,277 @@ +# -*- coding: utf-8 -*- require 'spec_helper' describe API::API, api: true do include ApiHelpers + include Gitlab::CurrentSettings let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } 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, author: user, project: project, title: 'example') } let(:project_member) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) } + let(:user4) { create(:user) } + let(:project3) do + create(:project, + 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) + end + let(:project_member3) do + create(:project_member, + user: user4, + project: project3, + access_level: ProjectMember::MASTER) + end + let(:project4) do + create(:project, + name: 'third_project', + path: 'third_project', + creator_id: user4.id, + namespace: user4.namespace) + end - describe "GET /projects" do + describe 'GET /projects' do before { project } - context "when unauthenticated" do - it "should return authentication error" do - get api("/projects") - response.status.should == 401 + context 'when unauthenticated' do + it 'should return authentication error' do + get api('/projects') + expect(response.status).to eq(401) end end - context "when authenticated" do - it "should return an array of projects" do - get api("/projects", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['name'].should == project.name - json_response.first['owner']['username'].should == user.username + context 'when authenticated' do + it 'should return an array of projects' do + get api('/projects', user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project.name) + expect(json_response.first['owner']['username']).to eq(user.username) end - end - end - describe "GET /projects/all" do - before { project } - - context "when unauthenticated" do - it "should return authentication error" do - get api("/projects/all") - response.status.should == 401 + it 'should include the project labels as the tag_list' do + get api('/projects', user) + expect(response.status).to eq 200 + expect(json_response).to be_an Array + expect(json_response.first.keys).to include('tag_list') end - end - context "when authenticated as regular user" do - it "should return authentication error" do - get api("/projects/all", user) - response.status.should == 403 + context 'and using search' do + it 'should return searched project' do + get api('/projects', user), { search: project.name } + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + end end - end - context "when authenticated as admin" do - it "should return an array of all projects" do - get api("/projects/all", admin) - response.status.should == 200 - json_response.should be_an Array - project_name = project.name + context 'and using sorting' do + before do + project2 + project3 + end - json_response.detect { - |project| project['name'] == project_name - }['name'].should == project_name + it 'should return the correct order when sorted by id' do + get api('/projects', user), { order_by: 'id', sort: 'desc'} + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(project3.id) + end - json_response.detect { - |project| project['owner']['username'] == user.username - }['owner']['username'].should == user.username + it 'returns projects in the correct order when ci_enabled_first parameter is passed' do + [project, project2, project3].each{ |project| project.build_missing_services } + project2.gitlab_ci_service.update(active: true, token: "token", project_url: "url") + get api('/projects', user), { ci_enabled_first: 'true'} + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(project2.id) + end end end end - describe "POST /projects" do - context "maximum number of projects reached" do - before do - (1..user2.projects_limit).each do |project| - post api("/projects", user2), name: "foo#{project}" - end - end + describe 'GET /projects/all' do + before { project } - it "should not create new project" do - expect { - post api("/projects", user2), name: 'foo' - }.to change {Project.count}.by(0) + context 'when unauthenticated' do + it 'should return authentication error' do + get api('/projects/all') + expect(response.status).to eq(401) end end - it "should create new project without path" do - expect { post api("/projects", user), name: 'foo' }.to change {Project.count}.by(1) + context 'when authenticated as regular user' do + it 'should return authentication error' do + get api('/projects/all', user) + expect(response.status).to eq(403) + end end - it "should not create new project without name" do - expect { post api("/projects", user) }.to_not change {Project.count} - end + context 'when authenticated as admin' do + it 'should return an array of all projects' do + get api('/projects/all', admin) + expect(response.status).to eq(200) + expect(json_response).to be_an Array - it "should return a 400 error if name not given" do - post api("/projects", user) - response.status.should == 400 + expect(json_response).to satisfy do |response| + response.one? do |entry| + entry['name'] == project.name && + entry['owner']['username'] == user.username + end + end + end end + end - it "should create last project before reaching project limit" do - (1..user2.projects_limit-1).each { |p| post api("/projects", user2), name: "foo#{p}" } - post api("/projects", user2), name: "foo" - response.status.should == 201 + describe 'POST /projects' do + context 'maximum number of projects reached' do + it 'should not create new project and respond with 403' do + allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0) + expect { post api('/projects', user2), name: 'foo' }. + to change {Project.count}.by(0) + expect(response.status).to eq(403) + end end - it "should respond with 201 on success" do - post api("/projects", user), name: 'foo' - response.status.should == 201 + it 'should create new project without path and return 201' do + expect { post api('/projects', user), name: 'foo' }. + to change { Project.count }.by(1) + expect(response.status).to eq(201) end - it "should respond with 400 if name is not given" do - post api("/projects", user) - response.status.should == 400 + it 'should create last project before reaching project limit' do + allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1) + post api('/projects', user2), name: 'foo' + expect(response.status).to eq(201) end - it "should return a 403 error if project limit reached" do - (1..user.projects_limit).each do |p| - post api("/projects", user), name: "foo#{p}" - end - post api("/projects", user), name: 'bar' - response.status.should == 403 + it 'should not create new project without name and return 400' do + expect { post api('/projects', user) }.not_to change { Project.count } + expect(response.status).to eq(400) end it "should assign attributes to project" do project = attributes_for(:project, { path: 'camelCasePath', - description: Faker::Lorem.sentence, + description: FFaker::Lorem.sentence, issues_enabled: false, merge_requests_enabled: false, wiki_enabled: false }) - post api("/projects", user), project + post api('/projects', user), project project.each_pair do |k,v| - json_response[k.to_s].should == v + expect(json_response[k.to_s]).to eq(v) end end - it "should set a project as public" do + it 'should set a project as public' do project = attributes_for(:project, :public) - post api("/projects", user), project - json_response['public'].should be_true - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC + post api('/projects', user), project + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) end - it "should set a project as public using :public" do + it 'should set a project as public using :public' do project = attributes_for(:project, { public: true }) - post api("/projects", user), project - json_response['public'].should be_true - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC + post api('/projects', user), project + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) end - it "should set a project as internal" do + it 'should set a project as internal' do project = attributes_for(:project, :internal) - post api("/projects", user), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL + post api('/projects', user), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) end - it "should set a project as internal overriding :public" do + it 'should set a project as internal overriding :public' do project = attributes_for(:project, :internal, { public: true }) - post api("/projects", user), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL + post api('/projects', user), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) end - it "should set a project as private" do + it 'should set a project as private' do project = attributes_for(:project, :private) - post api("/projects", user), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE + post api('/projects', user), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) end - it "should set a project as private using :public" do + it 'should set a project as private using :public' do project = attributes_for(:project, { public: false }) - post api("/projects", user), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE + post api('/projects', user), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + context 'when a visibility level is restricted' do + before do + @project = attributes_for(:project, { public: true }) + allow_any_instance_of(ApplicationSetting).to( + receive(:restricted_visibility_levels).and_return([20]) + ) + end + + it 'should not allow a non-admin to use a restricted visibility level' do + post api('/projects', user), @project + expect(response.status).to eq(400) + expect(json_response['message']['visibility_level'].first).to( + match('restricted by your GitLab administrator') + ) + end + + it 'should allow an admin to override restricted visibility settings' do + post api('/projects', admin), @project + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to( + eq(Gitlab::VisibilityLevel::PUBLIC) + ) + end end end - describe "POST /projects/user/:id" do + describe 'POST /projects/user/:id' do before { project } before { admin } - it "should create new project without path" do + it 'should create new project without path and return 201' do expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) + expect(response.status).to eq(201) end - it "should not create new project without name" do - expect { post api("/projects/user/#{user.id}", admin) }.to_not change {Project.count} - end - - it "should respond with 201 on success" do - post api("/projects/user/#{user.id}", admin), name: 'foo' - response.status.should == 201 - end + it 'should respond with 400 on failure and not project' do + expect { post api("/projects/user/#{user.id}", admin) }. + not_to change { Project.count } - it 'should respond with 400 on failure' do - post api("/projects/user/#{user.id}", admin) - response.status.should == 400 - json_response['message']['creator'].should == ['can\'t be blank'] - json_response['message']['namespace'].should == ['can\'t be blank'] - json_response['message']['name'].should == [ + expect(response.status).to eq(400) + expect(json_response['message']['name']).to eq([ 'can\'t be blank', 'is too short (minimum is 0 characters)', - Gitlab::Regex.project_regex_message - ] - json_response['message']['path'].should == [ + Gitlab::Regex.project_name_regex_message + ]) + expect(json_response['message']['path']).to eq([ 'can\'t be blank', 'is too short (minimum is 0 characters)', - Gitlab::Regex.send(:default_regex_message) - ] + Gitlab::Regex.send(:project_path_regex_message) + ]) end - it "should assign attributes to project" do + it 'should assign attributes to project' do project = attributes_for(:project, { - description: Faker::Lorem.sentence, + description: FFaker::Lorem.sentence, issues_enabled: false, merge_requests_enabled: false, wiki_enabled: false @@ -224,227 +281,217 @@ describe API::API, api: true do project.each_pair do |k,v| next if k == :path - json_response[k.to_s].should == v + expect(json_response[k.to_s]).to eq(v) end end - it "should set a project as public" do + it 'should set a project as public' do project = attributes_for(:project, :public) post api("/projects/user/#{user.id}", admin), project - json_response['public'].should be_true - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) end - it "should set a project as public using :public" do + it 'should set a project as public using :public' do project = attributes_for(:project, { public: true }) post api("/projects/user/#{user.id}", admin), project - json_response['public'].should be_true - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) end - it "should set a project as internal" do + it 'should set a project as internal' do project = attributes_for(:project, :internal) post api("/projects/user/#{user.id}", admin), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) end - it "should set a project as internal overriding :public" do + it 'should set a project as internal overriding :public' do project = attributes_for(:project, :internal, { public: true }) post api("/projects/user/#{user.id}", admin), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) end - it "should set a project as private" do + it 'should set a project as private' do project = attributes_for(:project, :private) post api("/projects/user/#{user.id}", admin), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) end - it "should set a project as private using :public" do + it 'should set a project as private using :public' do project = attributes_for(:project, { public: false }) post api("/projects/user/#{user.id}", admin), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) end end - describe "GET /projects/:id" do + describe 'GET /projects/:id' do before { project } before { project_member } - it "should return a project by id" do + it 'should return a project by id' do get api("/projects/#{project.id}", user) - response.status.should == 200 - json_response['name'].should == project.name - json_response['owner']['username'].should == user.username + expect(response.status).to eq(200) + expect(json_response['name']).to eq(project.name) + expect(json_response['owner']['username']).to eq(user.username) end - it "should return a project by path name" do + it 'should return a project by path name' do get api("/projects/#{project.id}", user) - response.status.should == 200 - json_response['name'].should == project.name + expect(response.status).to eq(200) + expect(json_response['name']).to eq(project.name) end - it "should return a 404 error if not found" do - get api("/projects/42", user) - response.status.should == 404 - json_response['message'].should == '404 Not Found' + it 'should return a 404 error if not found' do + get api('/projects/42', user) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Project Not Found') end - it "should return a 404 error if user is not a member" do + it 'should return a 404 error if user is not a member' do other_user = create(:user) get api("/projects/#{project.id}", other_user) - response.status.should == 404 + expect(response.status).to eq(404) end describe 'permissions' do context 'personal project' do - before do + it 'Sets project access and returns 200' do project.team << [user, :master] get api("/projects/#{project.id}", user) - end - it { response.status.should == 200 } - it { json_response['permissions']["project_access"]["access_level"].should == Gitlab::Access::MASTER } - it { json_response['permissions']["group_access"].should be_nil } + expect(response.status).to eq(200) + expect(json_response['permissions']['project_access']['access_level']). + to eq(Gitlab::Access::MASTER) + expect(json_response['permissions']['group_access']).to be_nil + end end context 'group project' do - before do + it 'should set the owner and return 200' do project2 = create(:project, group: create(:group)) project2.group.add_owner(user) get api("/projects/#{project2.id}", user) - end - it { response.status.should == 200 } - it { json_response['permissions']["project_access"].should be_nil } - it { json_response['permissions']["group_access"]["access_level"].should == Gitlab::Access::OWNER } + expect(response.status).to eq(200) + expect(json_response['permissions']['project_access']).to be_nil + expect(json_response['permissions']['group_access']['access_level']). + to eq(Gitlab::Access::OWNER) + end end end end - describe "GET /projects/:id/events" do - before { project_member } + describe 'GET /projects/:id/events' do + before { project_member2 } - it "should return a project events" do + it 'should return a project events' do get api("/projects/#{project.id}/events", user) - response.status.should == 200 + expect(response.status).to eq(200) json_event = json_response.first - json_event['action_name'].should == 'joined' - json_event['project_id'].to_i.should == project.id - json_event['author_username'].should == user.username + expect(json_event['action_name']).to eq('joined') + expect(json_event['project_id'].to_i).to eq(project.id) + expect(json_event['author_username']).to eq(user3.username) end - it "should return a 404 error if not found" do - get api("/projects/42/events", user) - response.status.should == 404 - json_response['message'].should == '404 Not Found' + it 'should return a 404 error if not found' do + get api('/projects/42/events', user) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Project Not Found') end - it "should return a 404 error if user is not a member" do + it 'should return a 404 error if user is not a member' do other_user = create(:user) get api("/projects/#{project.id}/events", other_user) - response.status.should == 404 + expect(response.status).to eq(404) end end - describe "GET /projects/:id/snippets" do + describe 'GET /projects/:id/snippets' do before { snippet } - it "should return an array of project snippets" do + it 'should return an array of project snippets' do get api("/projects/#{project.id}/snippets", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['title'].should == snippet.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(snippet.title) end end - describe "GET /projects/:id/snippets/:snippet_id" do - it "should return a project snippet" do + describe 'GET /projects/:id/snippets/:snippet_id' do + it 'should return a project snippet' do get api("/projects/#{project.id}/snippets/#{snippet.id}", user) - response.status.should == 200 - json_response['title'].should == snippet.title + expect(response.status).to eq(200) + expect(json_response['title']).to eq(snippet.title) end - it "should return a 404 error if snippet id not found" do + it 'should return a 404 error if snippet id not found' do get api("/projects/#{project.id}/snippets/1234", user) - response.status.should == 404 + expect(response.status).to eq(404) end end - describe "POST /projects/:id/snippets" do - it "should create a new project snippet" do + describe 'POST /projects/:id/snippets' do + it 'should create a new project snippet' do post api("/projects/#{project.id}/snippets", user), - title: 'api test', file_name: 'sample.rb', code: 'test' - response.status.should == 201 - json_response['title'].should == 'api test' + title: 'api test', file_name: 'sample.rb', code: 'test', + visibility_level: '0' + expect(response.status).to eq(201) + expect(json_response['title']).to eq('api test') end - it "should return a 400 error if title is not given" do - post api("/projects/#{project.id}/snippets", user), - file_name: 'sample.rb', code: 'test' - response.status.should == 400 - end - - it "should return a 400 error if file_name not given" do - post api("/projects/#{project.id}/snippets", user), - title: 'api test', code: 'test' - response.status.should == 400 - end - - it "should return a 400 error if code not given" do - post api("/projects/#{project.id}/snippets", user), - title: 'api test', file_name: 'sample.rb' - response.status.should == 400 + it 'should return a 400 error if invalid snippet is given' do + post api("/projects/#{project.id}/snippets", user) + expect(status).to eq(400) end end - describe "PUT /projects/:id/snippets/:shippet_id" do - it "should update an existing project snippet" do + describe 'PUT /projects/:id/snippets/:shippet_id' do + it 'should update an existing project snippet' do put api("/projects/#{project.id}/snippets/#{snippet.id}", user), code: 'updated code' - response.status.should == 200 - json_response['title'].should == 'example' - snippet.reload.content.should == 'updated code' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('example') + expect(snippet.reload.content).to eq('updated code') end - it "should update an existing project snippet with new title" do + it 'should update an existing project snippet with new title' do put api("/projects/#{project.id}/snippets/#{snippet.id}", user), title: 'other api test' - response.status.should == 200 - json_response['title'].should == 'other api test' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('other api test') end end - describe "DELETE /projects/:id/snippets/:snippet_id" do + describe 'DELETE /projects/:id/snippets/:snippet_id' do before { snippet } - it "should delete existing project snippet" do + it 'should delete existing project snippet' do expect { delete api("/projects/#{project.id}/snippets/#{snippet.id}", user) }.to change { Snippet.count }.by(-1) - response.status.should == 200 + expect(response.status).to eq(200) end it 'should return 404 when deleting unknown snippet id' do delete api("/projects/#{project.id}/snippets/1234", user) - response.status.should == 404 + expect(response.status).to eq(404) end end - describe "GET /projects/:id/snippets/:snippet_id/raw" do - it "should get a raw project snippet" do + describe 'GET /projects/:id/snippets/:snippet_id/raw' do + it 'should get a raw project snippet' do get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user) - response.status.should == 200 + expect(response.status).to eq(200) end - it "should return a 404 error if raw project snippet not found" do + it 'should return a 404 error if raw project snippet not found' do get api("/projects/#{project.id}/snippets/5555/raw", user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -452,51 +499,51 @@ describe API::API, api: true do let(:deploy_keys_project) { create(:deploy_keys_project, project: project) } let(:deploy_key) { deploy_keys_project.deploy_key } - describe "GET /projects/:id/keys" do + describe 'GET /projects/:id/keys' do before { deploy_key } - it "should return array of ssh keys" do + it 'should return array of ssh keys' do get api("/projects/#{project.id}/keys", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['title'].should == deploy_key.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(deploy_key.title) end end - describe "GET /projects/:id/keys/:key_id" do - it "should return a single key" do + describe 'GET /projects/:id/keys/:key_id' do + it 'should return a single key' do get api("/projects/#{project.id}/keys/#{deploy_key.id}", user) - response.status.should == 200 - json_response['title'].should == deploy_key.title + expect(response.status).to eq(200) + expect(json_response['title']).to eq(deploy_key.title) end - it "should return 404 Not Found with invalid ID" do + it 'should return 404 Not Found with invalid ID' do get api("/projects/#{project.id}/keys/404", user) - response.status.should == 404 + expect(response.status).to eq(404) end end - describe "POST /projects/:id/keys" do - it "should not create an invalid ssh key" do - post api("/projects/#{project.id}/keys", user), { title: "invalid key" } - response.status.should == 400 - json_response['message']['key'].should == [ + describe 'POST /projects/:id/keys' do + it 'should not create an invalid ssh key' do + post api("/projects/#{project.id}/keys", user), { title: 'invalid key' } + expect(response.status).to eq(400) + expect(json_response['message']['key']).to eq([ 'can\'t be blank', 'is too short (minimum is 0 characters)', 'is invalid' - ] + ]) end it 'should not create a key without title' do post api("/projects/#{project.id}/keys", user), key: 'some key' - response.status.should == 400 - json_response['message']['title'].should == [ + expect(response.status).to eq(400) + expect(json_response['message']['title']).to eq([ 'can\'t be blank', 'is too short (minimum is 0 characters)' - ] + ]) end - it "should create new ssh key" do + it 'should create new ssh key' do key_attrs = attributes_for :key expect { post api("/projects/#{project.id}/keys", user), key_attrs @@ -504,18 +551,18 @@ describe API::API, api: true do end end - describe "DELETE /projects/:id/keys/:key_id" do + describe 'DELETE /projects/:id/keys/:key_id' do before { deploy_key } - it "should delete existing key" do + it 'should delete existing key' do expect { delete api("/projects/#{project.id}/keys/#{deploy_key.id}", user) }.to change{ project.deploy_keys.count }.by(-1) end - it "should return 404 Not Found with invalid ID" do + it 'should return 404 Not Found with invalid ID' do delete api("/projects/#{project.id}/keys/404", user) - response.status.should == 404 + expect(response.status).to eq(404) end end end @@ -524,70 +571,70 @@ describe API::API, api: true do let(:project_fork_target) { create(:project) } let(:project_fork_source) { create(:project, :public) } - describe "POST /projects/:id/fork/:forked_from_id" do + describe 'POST /projects/:id/fork/:forked_from_id' do let(:new_project_fork_source) { create(:project, :public) } it "shouldn't available for non admin users" do post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user) - response.status.should == 403 + expect(response.status).to eq(403) end - it "should allow project to be forked from an existing project" do - project_fork_target.forked?.should_not be_true + it 'should allow project to be forked from an existing project' do + expect(project_fork_target.forked?).not_to be_truthy post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) - response.status.should == 201 + expect(response.status).to eq(201) project_fork_target.reload - project_fork_target.forked_from_project.id.should == project_fork_source.id - project_fork_target.forked_project_link.should_not be_nil - project_fork_target.forked?.should be_true + expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) + expect(project_fork_target.forked_project_link).not_to be_nil + expect(project_fork_target.forked?).to be_truthy end - it "should fail if forked_from project which does not exist" do + it 'should fail if forked_from project which does not exist' do post api("/projects/#{project_fork_target.id}/fork/9999", admin) - response.status.should == 404 + expect(response.status).to eq(404) end - it "should fail with 409 if already forked" do + it 'should fail with 409 if already forked' do post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) project_fork_target.reload - project_fork_target.forked_from_project.id.should == project_fork_source.id + expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) post api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin) - response.status.should == 409 + expect(response.status).to eq(409) project_fork_target.reload - project_fork_target.forked_from_project.id.should == project_fork_source.id - project_fork_target.forked?.should be_true + expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) + expect(project_fork_target.forked?).to be_truthy end end - describe "DELETE /projects/:id/fork" do + describe 'DELETE /projects/:id/fork' do it "shouldn't available for non admin users" do delete api("/projects/#{project_fork_target.id}/fork", user) - response.status.should == 403 + expect(response.status).to eq(403) end - it "should make forked project unforked" do + it 'should make forked project unforked' do post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) project_fork_target.reload - project_fork_target.forked_from_project.should_not be_nil - project_fork_target.forked?.should be_true + expect(project_fork_target.forked_from_project).not_to be_nil + expect(project_fork_target.forked?).to be_truthy delete api("/projects/#{project_fork_target.id}/fork", admin) - response.status.should == 200 + expect(response.status).to eq(200) project_fork_target.reload - project_fork_target.forked_from_project.should be_nil - project_fork_target.forked?.should_not be_true + expect(project_fork_target.forked_from_project).to be_nil + expect(project_fork_target.forked?).not_to be_truthy end - it "should be idempotent if not forked" do - project_fork_target.forked_from_project.should be_nil + it 'should be idempotent if not forked' do + expect(project_fork_target.forked_from_project).to be_nil delete api("/projects/#{project_fork_target.id}/fork", admin) - response.status.should == 200 - project_fork_target.reload.forked_from_project.should be_nil + expect(response.status).to eq(200) + expect(project_fork_target.reload.forked_from_project).to be_nil end end end - describe "GET /projects/search/:query" do + describe 'GET /projects/search/:query' do let!(:query) { 'query'} let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } @@ -599,73 +646,180 @@ describe API::API, api: true do let!(:public) { create(:empty_project, :public, name: "public #{query}") } let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') } - context "when unauthenticated" do - it "should return authentication error" do + context 'when unauthenticated' do + it 'should return authentication error' do get api("/projects/search/#{query}") - response.status.should == 401 + expect(response.status).to eq(401) end end - context "when authenticated" do - it "should return an array of projects" do + context 'when authenticated' do + it 'should return an array of projects' do get api("/projects/search/#{query}",user) - response.status.should == 200 - json_response.should be_an Array - json_response.size.should == 6 - json_response.each {|project| project['name'].should =~ /.*query.*/} + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(6) + json_response.each {|project| expect(project['name']).to match(/.*query.*/)} end end - context "when authenticated as a different user" do - it "should return matching public projects" do + context 'when authenticated as a different user' do + it 'should return matching public projects' do get api("/projects/search/#{query}", user2) - response.status.should == 200 - json_response.should be_an Array - json_response.size.should == 2 - json_response.each {|project| project['name'].should =~ /(internal|public) query/} + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + json_response.each {|project| expect(project['name']).to match(/(internal|public) query/)} end end end - describe "DELETE /projects/:id" do - context "when authenticated as user" do - it "should remove project" do - expect(GitlabShellWorker).to( - receive(:perform_async).with(:remove_repository, - /#{project.path_with_namespace}/) - ).twice + describe 'PUT /projects/:id̈́' do + before { project } + before { user } + before { user3 } + before { user4 } + before { project3 } + before { project4 } + before { project_member3 } + before { project_member2 } + + context 'when unauthenticated' do + it 'should return authentication error' do + project_param = { name: 'bar' } + put api("/projects/#{project.id}"), project_param + expect(response.status).to eq(401) + end + end + + context 'when authenticated as project owner' do + it 'should update name' do + project_param = { name: 'bar' } + put api("/projects/#{project.id}", user), project_param + expect(response.status).to eq(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'should update visibility_level' do + project_param = { visibility_level: 20 } + put api("/projects/#{project3.id}", user), project_param + expect(response.status).to eq(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'should not update name to existing name' do + project_param = { name: project3.name } + put api("/projects/#{project.id}", user), project_param + expect(response.status).to eq(400) + expect(json_response['message']['name']).to eq(['has already been taken']) + end + + it 'should update path & name to existing path & name in different namespace' do + project_param = { path: project4.path, name: project4.name } + put api("/projects/#{project3.id}", user), project_param + expect(response.status).to eq(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + end + + context 'when authenticated as project master' do + it 'should update path' do + project_param = { path: 'bar' } + put api("/projects/#{project3.id}", user4), project_param + expect(response.status).to eq(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'should update other attributes' do + project_param = { issues_enabled: true, + wiki_enabled: true, + snippets_enabled: true, + merge_requests_enabled: true, + description: 'new description' } + + put api("/projects/#{project3.id}", user4), project_param + expect(response.status).to eq(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'should not update path to existing path' do + project_param = { path: project.path } + put api("/projects/#{project3.id}", user4), project_param + expect(response.status).to eq(400) + expect(json_response['message']['path']).to eq(['has already been taken']) + end + + it 'should not update name' do + project_param = { name: 'bar' } + put api("/projects/#{project3.id}", user4), project_param + expect(response.status).to eq(403) + end + + it 'should not update visibility_level' do + project_param = { visibility_level: 20 } + put api("/projects/#{project3.id}", user4), project_param + expect(response.status).to eq(403) + end + end + + context 'when authenticated as project developer' do + it 'should not update other attributes' do + project_param = { path: 'bar', + issues_enabled: true, + wiki_enabled: true, + snippets_enabled: true, + merge_requests_enabled: true, + description: 'new description' } + put api("/projects/#{project.id}", user3), project_param + expect(response.status).to eq(403) + end + end + end + describe 'DELETE /projects/:id' do + context 'when authenticated as user' do + it 'should remove project' do delete api("/projects/#{project.id}", user) - response.status.should == 200 + expect(response.status).to eq(200) end - it "should not remove a project if not an owner" do + it 'should not remove a project if not an owner' do user3 = create(:user) project.team << [user3, :developer] delete api("/projects/#{project.id}", user3) - response.status.should == 403 + expect(response.status).to eq(403) end - it "should not remove a non existing project" do - delete api("/projects/1328", user) - response.status.should == 404 + it 'should not remove a non existing project' do + delete api('/projects/1328', user) + expect(response.status).to eq(404) end - it "should not remove a project not attached to user" do + it 'should not remove a project not attached to user' do delete api("/projects/#{project.id}", user2) - response.status.should == 404 + expect(response.status).to eq(404) end end - context "when authenticated as admin" do - it "should remove any existing project" do + context 'when authenticated as admin' do + it 'should remove any existing project' do delete api("/projects/#{project.id}", admin) - response.status.should == 200 + expect(response.status).to eq(200) end - it "should not remove a non existing project" do - delete api("/projects/1328", admin) - response.status.should == 404 + it 'should not remove a non existing project' do + delete api('/projects/1328', admin) + expect(response.status).to eq(404) end end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index beae71c02d9..09a79553f72 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -11,14 +11,12 @@ describe API::API, api: true do let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } - before { project.team << [user, :reporter] } - describe "GET /projects/:id/repository/tags" do it "should return an array of project tags" do get api("/projects/#{project.id}/repository/tags", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['name'].should == project.repo.tags.sort_by(&:name).reverse.first.name + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project.repo.tags.sort_by(&:name).reverse.first.name) end end @@ -29,8 +27,8 @@ describe API::API, api: true do tag_name: 'v7.0.1', ref: 'master' - response.status.should == 201 - json_response['name'].should == 'v7.0.1' + expect(response.status).to eq(201) + expect(json_response['name']).to eq('v7.0.1') end end @@ -46,9 +44,9 @@ describe API::API, api: true do ref: 'master', message: 'Release 7.1.0' - response.status.should == 201 - json_response['name'].should == 'v7.1.0' - json_response['message'].should == 'Release 7.1.0' + expect(response.status).to eq(201) + expect(json_response['name']).to eq('v7.1.0') + expect(json_response['message']).to eq('Release 7.1.0') end end @@ -56,35 +54,35 @@ describe API::API, api: true do post api("/projects/#{project.id}/repository/tags", user2), tag_name: 'v1.9.0', ref: '621491c677087aa243f165eab467bfdfbee00be1' - response.status.should == 403 + expect(response.status).to eq(403) end it 'should return 400 if tag name is invalid' do post api("/projects/#{project.id}/repository/tags", user), tag_name: 'v 1.0.0', ref: 'master' - response.status.should == 400 - json_response['message'].should == 'Tag name invalid' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Tag name invalid') end it 'should return 400 if tag already exists' do post api("/projects/#{project.id}/repository/tags", user), tag_name: 'v8.0.0', ref: 'master' - response.status.should == 201 + expect(response.status).to eq(201) post api("/projects/#{project.id}/repository/tags", user), tag_name: 'v8.0.0', ref: 'master' - response.status.should == 400 - json_response['message'].should == 'Tag already exists' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Tag already exists') end it 'should return 400 if ref name is invalid' do post api("/projects/#{project.id}/repository/tags", user), tag_name: 'mytag', ref: 'foo' - response.status.should == 400 - json_response['message'].should == 'Invalid reference name' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Invalid reference name') end end @@ -94,19 +92,27 @@ describe API::API, api: true do it "should return project commits" do get api("/projects/#{project.id}/repository/tree", user) - response.status.should == 200 + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq('encoding') + expect(json_response.first['type']).to eq('tree') + expect(json_response.first['mode']).to eq('040000') + end + + it 'should return a 404 for unknown ref' do + get api("/projects/#{project.id}/repository/tree?ref_name=foo", user) + expect(response.status).to eq(404) - json_response.should be_an Array - json_response.first['name'].should == 'encoding' - json_response.first['type'].should == 'tree' - json_response.first['mode'].should == '040000' + expect(json_response).to be_an Object + json_response['message'] == '404 Tree Not Found' end end context "unauthorized user" do it "should not return project commits" do get api("/projects/#{project.id}/repository/tree") - response.status.should == 401 + expect(response.status).to eq(401) end end end @@ -114,36 +120,44 @@ describe API::API, api: true do describe "GET /projects/:id/repository/blobs/:sha" do it "should get the raw file contents" do get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return 404 for invalid branch_name" do get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user) - response.status.should == 404 + expect(response.status).to eq(404) end it "should return 404 for invalid file" do get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user) - response.status.should == 404 + expect(response.status).to eq(404) end it "should return a 400 error if filepath is missing" do get api("/projects/#{project.id}/repository/blobs/master", user) - response.status.should == 400 + expect(response.status).to eq(400) end end describe "GET /projects/:id/repository/commits/:sha/blob" do it "should get the raw file contents" do get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user) - response.status.should == 200 + expect(response.status).to eq(200) end end describe "GET /projects/:id/repository/raw_blobs/:sha" do it "should get the raw file contents" do get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user) - response.status.should == 200 + expect(response.status).to eq(200) + end + + it 'should return a 404 for unknown blob' do + get api("/projects/#{project.id}/repository/raw_blobs/123456", user) + expect(response.status).to eq(404) + + expect(json_response).to be_an Object + json_response['message'] == '404 Blob Not Found' end end @@ -151,83 +165,83 @@ describe API::API, api: true do it "should get the archive" do get api("/projects/#{project.id}/repository/archive", user) repo_name = project.repository.name.gsub("\.git", "") - response.status.should == 200 - response.headers['Content-Disposition'].should =~ /filename\=\"#{repo_name}\-[^\.]+\.tar.gz\"/ - response.content_type.should == MIME::Types.type_for('file.tar.gz').first.content_type + expect(response.status).to eq(200) + expect(response.headers['Content-Disposition']).to match(/filename\=\"#{repo_name}\-[^\.]+\.tar.gz\"/) + expect(response.content_type).to eq(MIME::Types.type_for('file.tar.gz').first.content_type) end it "should get the archive.zip" do get api("/projects/#{project.id}/repository/archive.zip", user) repo_name = project.repository.name.gsub("\.git", "") - response.status.should == 200 - response.headers['Content-Disposition'].should =~ /filename\=\"#{repo_name}\-[^\.]+\.zip\"/ - response.content_type.should == MIME::Types.type_for('file.zip').first.content_type + expect(response.status).to eq(200) + expect(response.headers['Content-Disposition']).to match(/filename\=\"#{repo_name}\-[^\.]+\.zip\"/) + expect(response.content_type).to eq(MIME::Types.type_for('file.zip').first.content_type) end it "should get the archive.tar.bz2" do get api("/projects/#{project.id}/repository/archive.tar.bz2", user) repo_name = project.repository.name.gsub("\.git", "") - response.status.should == 200 - response.headers['Content-Disposition'].should =~ /filename\=\"#{repo_name}\-[^\.]+\.tar.bz2\"/ - response.content_type.should == MIME::Types.type_for('file.tar.bz2').first.content_type + expect(response.status).to eq(200) + expect(response.headers['Content-Disposition']).to match(/filename\=\"#{repo_name}\-[^\.]+\.tar.bz2\"/) + expect(response.content_type).to eq(MIME::Types.type_for('file.tar.bz2').first.content_type) end it "should return 404 for invalid sha" do get api("/projects/#{project.id}/repository/archive/?sha=xxx", user) - response.status.should == 404 + expect(response.status).to eq(404) end end describe 'GET /projects/:id/repository/compare' do it "should compare branches" do get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'feature' - response.status.should == 200 - json_response['commits'].should be_present - json_response['diffs'].should be_present + expect(response.status).to eq(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present end it "should compare tags" do get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.0', to: 'v1.1.0' - response.status.should == 200 - json_response['commits'].should be_present - json_response['diffs'].should be_present + expect(response.status).to eq(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present end it "should compare commits" do get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.id, to: sample_commit.parent_id - response.status.should == 200 - json_response['commits'].should be_empty - json_response['diffs'].should be_empty - json_response['compare_same_ref'].should be_false + expect(response.status).to eq(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_falsey end it "should compare commits in reverse order" do get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.parent_id, to: sample_commit.id - response.status.should == 200 - json_response['commits'].should be_present - json_response['diffs'].should be_present + expect(response.status).to eq(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present end it "should compare same refs" do get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master' - response.status.should == 200 - json_response['commits'].should be_empty - json_response['diffs'].should be_empty - json_response['compare_same_ref'].should be_true + expect(response.status).to eq(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_truthy end end describe 'GET /projects/:id/repository/contributors' do it 'should return valid data' do get api("/projects/#{project.id}/repository/contributors", user) - response.status.should == 200 - json_response.should be_an Array + expect(response.status).to eq(200) + expect(json_response).to be_an Array contributor = json_response.first - contributor['email'].should == 'dmitriy.zaporozhets@gmail.com' - contributor['name'].should == 'Dmitriy Zaporozhets' - contributor['commits'].should == 13 - contributor['additions'].should == 0 - contributor['deletions'].should == 0 + expect(contributor['email']).to eq('dmitriy.zaporozhets@gmail.com') + expect(contributor['name']).to eq('Dmitriy Zaporozhets') + expect(contributor['commits']).to eq(13) + expect(contributor['additions']).to eq(0) + expect(contributor['deletions']).to eq(0) end end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index d8282d0696b..51c543578df 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -9,13 +9,13 @@ describe API::API, api: true do it "should update gitlab-ci settings" do put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'secret-token', project_url: "http://ci.example.com/projects/1" - response.status.should == 200 + expect(response.status).to eq(200) end it "should return if required fields missing" do put api("/projects/#{project.id}/services/gitlab-ci", user), project_url: "http://ci.example.com/projects/1", active: true - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -23,8 +23,8 @@ describe API::API, api: true do it "should update gitlab-ci settings" do delete api("/projects/#{project.id}/services/gitlab-ci", user) - response.status.should == 200 - project.gitlab_ci_service.should be_nil + expect(response.status).to eq(200) + expect(project.gitlab_ci_service).to be_nil end end @@ -33,15 +33,15 @@ describe API::API, api: true do put api("/projects/#{project.id}/services/hipchat", user), token: 'secret-token', room: 'test' - response.status.should == 200 - project.hipchat_service.should_not be_nil + expect(response.status).to eq(200) + expect(project.hipchat_service).not_to be_nil end it 'should return if required fields missing' do put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'secret-token', active: true - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -49,8 +49,8 @@ describe API::API, api: true do it 'should delete hipchat settings' do delete api("/projects/#{project.id}/services/hipchat", user) - response.status.should == 200 - project.hipchat_service.should be_nil + expect(response.status).to eq(200) + expect(project.hipchat_service).to be_nil end end end diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb index 57b2e6cbd6a..fbd57b34a58 100644 --- a/spec/requests/api/session_spec.rb +++ b/spec/requests/api/session_spec.rb @@ -9,13 +9,13 @@ describe API::API, api: true do context "when valid password" do it "should return private token" do post api("/session"), email: user.email, password: '12345678' - response.status.should == 201 + expect(response.status).to eq(201) - json_response['email'].should == user.email - json_response['private_token'].should == user.private_token - json_response['is_admin'].should == user.is_admin? - json_response['can_create_project'].should == user.can_create_project? - json_response['can_create_group'].should == user.can_create_group? + expect(json_response['email']).to eq(user.email) + expect(json_response['private_token']).to eq(user.private_token) + expect(json_response['is_admin']).to eq(user.is_admin?) + expect(json_response['can_create_project']).to eq(user.can_create_project?) + expect(json_response['can_create_group']).to eq(user.can_create_group?) end end @@ -48,30 +48,30 @@ describe API::API, api: true do context "when invalid password" do it "should return authentication error" do post api("/session"), email: user.email, password: '123' - response.status.should == 401 + expect(response.status).to eq(401) - json_response['email'].should be_nil - json_response['private_token'].should be_nil + expect(json_response['email']).to be_nil + expect(json_response['private_token']).to be_nil end end context "when empty password" do it "should return authentication error" do post api("/session"), email: user.email - response.status.should == 401 + expect(response.status).to eq(401) - json_response['email'].should be_nil - json_response['private_token'].should be_nil + expect(json_response['email']).to be_nil + expect(json_response['private_token']).to be_nil end end context "when empty name" do it "should return authentication error" do post api("/session"), password: user.password - response.status.should == 401 + expect(response.status).to eq(401) - json_response['email'].should be_nil - json_response['private_token'].should be_nil + expect(json_response['email']).to be_nil + expect(json_response['private_token']).to be_nil end end end diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index 5784ae8c23a..2c691f72f15 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -13,23 +13,23 @@ describe API::API, api: true do context "when no user" do it "should return authentication error" do get api("/hooks") - response.status.should == 401 + expect(response.status).to eq(401) end end context "when not an admin" do it "should return forbidden error" do get api("/hooks", user) - response.status.should == 403 + expect(response.status).to eq(403) end end context "when authenticated as admin" do it "should return an array of hooks" do get api("/hooks", admin) - response.status.should == 200 - json_response.should be_an Array - json_response.first['url'].should == hook.url + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['url']).to eq(hook.url) end end end @@ -43,26 +43,26 @@ describe API::API, api: true do it "should respond with 400 if url not given" do post api("/hooks", admin) - response.status.should == 400 + expect(response.status).to eq(400) end it "should not create new hook without url" do expect { post api("/hooks", admin) - }.to_not change { SystemHook.count } + }.not_to change { SystemHook.count } end end describe "GET /hooks/:id" do it "should return hook by id" do get api("/hooks/#{hook.id}", admin) - response.status.should == 200 - json_response['event_name'].should == 'project_create' + expect(response.status).to eq(200) + expect(json_response['event_name']).to eq('project_create') end it "should return 404 on failure" do get api("/hooks/404", admin) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -75,7 +75,7 @@ describe API::API, api: true do it "should return success if hook id not found" do delete api("/hooks/12345", admin) - response.status.should == 200 + expect(response.status).to eq(200) end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 113a39b870e..327f3e6d23c 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -11,30 +11,30 @@ describe API::API, api: true do context "when unauthenticated" do it "should return authentication error" do get api("/users") - response.status.should == 401 + expect(response.status).to eq(401) end end context "when authenticated" do it "should return an array of users" do get api("/users", user) - response.status.should == 200 - json_response.should be_an Array + expect(response.status).to eq(200) + expect(json_response).to be_an Array username = user.username - json_response.detect { + expect(json_response.detect { |user| user['username'] == username - }['username'].should == username + }['username']).to eq(username) end end context "when admin" do it "should return an array of users" do get api("/users", admin) - response.status.should == 200 - json_response.should be_an Array - json_response.first.keys.should include 'email' - json_response.first.keys.should include 'extern_uid' - json_response.first.keys.should include 'can_create_project' + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first.keys).to include 'email' + expect(json_response.first.keys).to include 'identities' + expect(json_response.first.keys).to include 'can_create_project' end end end @@ -42,19 +42,19 @@ describe API::API, api: true do describe "GET /users/:id" do it "should return a user by id" do get api("/users/#{user.id}", user) - response.status.should == 200 - json_response['username'].should == user.username + expect(response.status).to eq(200) + expect(json_response['username']).to eq(user.username) end it "should return a 401 if unauthenticated" do get api("/users/9998") - response.status.should == 401 + expect(response.status).to eq(401) end it "should return a 404 error if user id not found" do get api("/users/9999", user) - response.status.should == 404 - json_response['message'].should == '404 Not found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') end end @@ -69,36 +69,36 @@ describe API::API, api: true do it "should create user with correct attributes" do post api('/users', admin), attributes_for(:user, admin: true, can_create_group: true) - response.status.should == 201 + expect(response.status).to eq(201) user_id = json_response['id'] new_user = User.find(user_id) - new_user.should_not == nil - new_user.admin.should == true - new_user.can_create_group.should == true + expect(new_user).not_to eq(nil) + expect(new_user.admin).to eq(true) + expect(new_user.can_create_group).to eq(true) end it "should create non-admin user" do post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false) - response.status.should == 201 + expect(response.status).to eq(201) user_id = json_response['id'] new_user = User.find(user_id) - new_user.should_not == nil - new_user.admin.should == false - new_user.can_create_group.should == false + expect(new_user).not_to eq(nil) + expect(new_user.admin).to eq(false) + expect(new_user.can_create_group).to eq(false) end it "should create non-admin users by default" do post api('/users', admin), attributes_for(:user) - response.status.should == 201 + expect(response.status).to eq(201) user_id = json_response['id'] new_user = User.find(user_id) - new_user.should_not == nil - new_user.admin.should == false + expect(new_user).not_to eq(nil) + expect(new_user.admin).to eq(false) end it "should return 201 Created on success" do post api("/users", admin), attributes_for(:user, projects_limit: 3) - response.status.should == 201 + expect(response.status).to eq(201) end it "should not create user with invalid email" do @@ -106,22 +106,27 @@ describe API::API, api: true do email: 'invalid email', password: 'password', name: 'test' - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return 400 error if name not given' do - post api('/users', admin), email: 'test@example.com', password: 'pass1234' - response.status.should == 400 + post api('/users', admin), attributes_for(:user).except(:name) + expect(response.status).to eq(400) end it 'should return 400 error if password not given' do - post api('/users', admin), email: 'test@example.com', name: 'test' - response.status.should == 400 + post api('/users', admin), attributes_for(:user).except(:password) + expect(response.status).to eq(400) end - it "should return 400 error if email not given" do - post api('/users', admin), password: 'pass1234', name: 'test' - response.status.should == 400 + it 'should return 400 error if email not given' do + post api('/users', admin), attributes_for(:user).except(:email) + expect(response.status).to eq(400) + end + + it 'should return 400 error if username not given' do + post api('/users', admin), attributes_for(:user).except(:username) + expect(response.status).to eq(400) end it 'should return 400 error if user does not validate' do @@ -132,20 +137,20 @@ describe API::API, api: true do name: 'test', bio: 'g' * 256, projects_limit: -1 - response.status.should == 400 - json_response['message']['password']. - should == ['is too short (minimum is 8 characters)'] - json_response['message']['bio']. - should == ['is too long (maximum is 255 characters)'] - json_response['message']['projects_limit']. - should == ['must be greater than or equal to 0'] - json_response['message']['username']. - should == [Gitlab::Regex.send(:default_regex_message)] + expect(response.status).to eq(400) + expect(json_response['message']['password']). + to eq(['is too short (minimum is 8 characters)']) + expect(json_response['message']['bio']). + to eq(['is too long (maximum is 255 characters)']) + expect(json_response['message']['projects_limit']). + to eq(['must be greater than or equal to 0']) + expect(json_response['message']['username']). + to eq([Gitlab::Regex.send(:namespace_regex_message)]) end it "shouldn't available for non admin users" do post api("/users", user), attributes_for(:user) - response.status.should == 403 + expect(response.status).to eq(403) end context 'with existing user' do @@ -165,8 +170,8 @@ describe API::API, api: true do password: 'password', username: 'foo' }.to change { User.count }.by(0) - response.status.should == 409 - json_response['message'].should == 'Email has already been taken' + expect(response.status).to eq(409) + expect(json_response['message']).to eq('Email has already been taken') end it 'should return 409 conflict error if same username exists' do @@ -177,34 +182,18 @@ describe API::API, api: true do password: 'password', username: 'test' end.to change { User.count }.by(0) - response.status.should == 409 - json_response['message'].should == 'Username has already been taken' + expect(response.status).to eq(409) + expect(json_response['message']).to eq('Username has already been taken') end end end describe "GET /users/sign_up" do - context 'enabled' do - before do - Gitlab.config.gitlab.stub(:signup_enabled).and_return(true) - end - it "should return sign up page if signup is enabled" do - get "/users/sign_up" - response.status.should == 200 - end - end - - context 'disabled' do - before do - Gitlab.config.gitlab.stub(:signup_enabled).and_return(false) - end - - it "should redirect to sign in page if signup is disabled" do - get "/users/sign_up" - response.status.should == 302 - response.should redirect_to(new_user_session_path) - end + it "should redirect to sign in page" do + get "/users/sign_up" + expect(response.status).to eq(302) + expect(response).to redirect_to(new_user_session_path) end end @@ -215,55 +204,55 @@ describe API::API, api: true do it "should update user with new bio" do put api("/users/#{user.id}", admin), {bio: 'new test bio'} - response.status.should == 200 - json_response['bio'].should == 'new test bio' - user.reload.bio.should == 'new test bio' + expect(response.status).to eq(200) + expect(json_response['bio']).to eq('new test bio') + expect(user.reload.bio).to eq('new test bio') end it 'should update user with his own email' do put api("/users/#{user.id}", admin), email: user.email - response.status.should == 200 - json_response['email'].should == user.email - user.reload.email.should == user.email + expect(response.status).to eq(200) + expect(json_response['email']).to eq(user.email) + expect(user.reload.email).to eq(user.email) end it 'should update user with his own username' do put api("/users/#{user.id}", admin), username: user.username - response.status.should == 200 - json_response['username'].should == user.username - user.reload.username.should == user.username + expect(response.status).to eq(200) + expect(json_response['username']).to eq(user.username) + expect(user.reload.username).to eq(user.username) end it "should update admin status" do put api("/users/#{user.id}", admin), {admin: true} - response.status.should == 200 - json_response['is_admin'].should == true - user.reload.admin.should == true + expect(response.status).to eq(200) + expect(json_response['is_admin']).to eq(true) + expect(user.reload.admin).to eq(true) end it "should not update admin status" do put api("/users/#{admin_user.id}", admin), {can_create_group: false} - response.status.should == 200 - json_response['is_admin'].should == true - admin_user.reload.admin.should == true - admin_user.can_create_group.should == false + expect(response.status).to eq(200) + expect(json_response['is_admin']).to eq(true) + expect(admin_user.reload.admin).to eq(true) + expect(admin_user.can_create_group).to eq(false) end it "should not allow invalid update" do put api("/users/#{user.id}", admin), {email: 'invalid email'} - response.status.should == 400 - user.reload.email.should_not == 'invalid email' + expect(response.status).to eq(400) + expect(user.reload.email).not_to eq('invalid email') end it "shouldn't available for non admin users" do put api("/users/#{user.id}", user), attributes_for(:user) - response.status.should == 403 + expect(response.status).to eq(403) end it "should return 404 for non-existing user" do put api("/users/999999", admin), {bio: 'update should fail'} - response.status.should == 404 - json_response['message'].should == '404 Not found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') end it 'should return 400 error if user does not validate' do @@ -274,15 +263,15 @@ describe API::API, api: true do name: 'test', bio: 'g' * 256, projects_limit: -1 - response.status.should == 400 - json_response['message']['password']. - should == ['is too short (minimum is 8 characters)'] - json_response['message']['bio']. - should == ['is too long (maximum is 255 characters)'] - json_response['message']['projects_limit']. - should == ['must be greater than or equal to 0'] - json_response['message']['username']. - should == [Gitlab::Regex.send(:default_regex_message)] + expect(response.status).to eq(400) + expect(json_response['message']['password']). + to eq(['is too short (minimum is 8 characters)']) + expect(json_response['message']['bio']). + to eq(['is too long (maximum is 255 characters)']) + expect(json_response['message']['projects_limit']). + to eq(['must be greater than or equal to 0']) + expect(json_response['message']['username']). + to eq([Gitlab::Regex.send(:namespace_regex_message)]) end context "with existing user" do @@ -294,15 +283,15 @@ describe API::API, api: true do it 'should return 409 conflict error if email address exists' do put api("/users/#{@user.id}", admin), email: 'test@example.com' - response.status.should == 409 - @user.reload.email.should == @user.email + expect(response.status).to eq(409) + expect(@user.reload.email).to eq(@user.email) end it 'should return 409 conflict error if username taken' do @user_id = User.all.last.id put api("/users/#{@user.id}", admin), username: 'test' - response.status.should == 409 - @user.reload.username.should == @user.username + expect(response.status).to eq(409) + expect(@user.reload.username).to eq(@user.username) end end end @@ -312,14 +301,14 @@ describe API::API, api: true do it "should not create invalid ssh key" do post api("/users/#{user.id}/keys", admin), { title: "invalid key" } - response.status.should == 400 - json_response['message'].should == '400 (Bad request) "key" not given' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "key" not given') end it 'should not create key without title' do post api("/users/#{user.id}/keys", admin), key: 'some key' - response.status.should == 400 - json_response['message'].should == '400 (Bad request) "title" not given' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "title" not given') end it "should create ssh key" do @@ -336,24 +325,24 @@ describe API::API, api: true do context 'when unauthenticated' do it 'should return authentication error' do get api("/users/#{user.id}/keys") - response.status.should == 401 + expect(response.status).to eq(401) end end context 'when authenticated' do it 'should return 404 for non-existing user' do get api('/users/999999/keys', admin) - response.status.should == 404 - json_response['message'].should == '404 User Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 User Not Found') end it 'should return array of ssh keys' do user.keys << key user.save get api("/users/#{user.id}/keys", admin) - response.status.should == 200 - json_response.should be_an Array - json_response.first['title'].should == key.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(key.title) end end end @@ -364,7 +353,7 @@ describe API::API, api: true do context 'when unauthenticated' do it 'should return authentication error' do delete api("/users/#{user.id}/keys/42") - response.status.should == 401 + expect(response.status).to eq(401) end end @@ -375,21 +364,21 @@ describe API::API, api: true do expect { delete api("/users/#{user.id}/keys/#{key.id}", admin) }.to change { user.keys.count }.by(-1) - response.status.should == 200 + expect(response.status).to eq(200) end it 'should return 404 error if user not found' do user.keys << key user.save delete api("/users/999999/keys/#{key.id}", admin) - response.status.should == 404 - json_response['message'].should == '404 User Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 User Not Found') end it 'should return 404 error if key not foud' do delete api("/users/#{user.id}/keys/42", admin) - response.status.should == 404 - json_response['message'].should == '404 Key Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Key Not Found') end end end @@ -399,42 +388,42 @@ describe API::API, api: true do it "should delete user" do delete api("/users/#{user.id}", admin) - response.status.should == 200 + expect(response.status).to eq(200) expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound - json_response['email'].should == user.email + expect(json_response['email']).to eq(user.email) end it "should not delete for unauthenticated user" do delete api("/users/#{user.id}") - response.status.should == 401 + expect(response.status).to eq(401) end it "shouldn't available for non admin users" do delete api("/users/#{user.id}", user) - response.status.should == 403 + expect(response.status).to eq(403) end it "should return 404 for non-existing user" do delete api("/users/999999", admin) - response.status.should == 404 - json_response['message'].should == '404 User Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 User Not Found') end end describe "GET /user" do it "should return current user" do get api("/user", user) - response.status.should == 200 - json_response['email'].should == user.email - json_response['is_admin'].should == user.is_admin? - json_response['can_create_project'].should == user.can_create_project? - json_response['can_create_group'].should == user.can_create_group? - json_response['projects_limit'].should == user.projects_limit + expect(response.status).to eq(200) + expect(json_response['email']).to eq(user.email) + expect(json_response['is_admin']).to eq(user.is_admin?) + expect(json_response['can_create_project']).to eq(user.can_create_project?) + expect(json_response['can_create_group']).to eq(user.can_create_group?) + expect(json_response['projects_limit']).to eq(user.projects_limit) end it "should return 401 error if user is unauthenticated" do get api("/user") - response.status.should == 401 + expect(response.status).to eq(401) end end @@ -442,7 +431,7 @@ describe API::API, api: true do context "when unauthenticated" do it "should return authentication error" do get api("/user/keys") - response.status.should == 401 + expect(response.status).to eq(401) end end @@ -451,9 +440,9 @@ describe API::API, api: true do user.keys << key user.save get api("/user/keys", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first["title"].should == key.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first["title"]).to eq(key.title) end end end @@ -463,14 +452,14 @@ describe API::API, api: true do user.keys << key user.save get api("/user/keys/#{key.id}", user) - response.status.should == 200 - json_response["title"].should == key.title + expect(response.status).to eq(200) + expect(json_response["title"]).to eq(key.title) end it "should return 404 Not Found within invalid ID" do get api("/user/keys/42", user) - response.status.should == 404 - json_response['message'].should == '404 Not found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') end it "should return 404 error if admin accesses user's ssh key" do @@ -478,8 +467,8 @@ describe API::API, api: true do user.save admin get api("/user/keys/#{key.id}", admin) - response.status.should == 404 - json_response['message'].should == '404 Not found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') end end @@ -489,29 +478,29 @@ describe API::API, api: true do expect { post api("/user/keys", user), key_attrs }.to change{ user.keys.count }.by(1) - response.status.should == 201 + expect(response.status).to eq(201) end it "should return a 401 error if unauthorized" do post api("/user/keys"), title: 'some title', key: 'some key' - response.status.should == 401 + expect(response.status).to eq(401) end it "should not create ssh key without key" do post api("/user/keys", user), title: 'title' - response.status.should == 400 - json_response['message'].should == '400 (Bad request) "key" not given' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "key" not given') end it 'should not create ssh key without title' do post api('/user/keys', user), key: 'some key' - response.status.should == 400 - json_response['message'].should == '400 (Bad request) "title" not given' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "title" not given') end it "should not create ssh key without title" do post api("/user/keys", user), key: "somekey" - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -522,19 +511,19 @@ describe API::API, api: true do expect { delete api("/user/keys/#{key.id}", user) }.to change{user.keys.count}.by(-1) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return success if key ID not found" do delete api("/user/keys/42", user) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return 401 error if unauthorized" do user.keys << key user.save delete api("/user/keys/#{key.id}") - response.status.should == 401 + expect(response.status).to eq(401) end end end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 7fe18ff47c3..bf8abcfb00f 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -12,47 +12,47 @@ require 'spec_helper' # DELETE /admin/users/:id(.:format) admin/users#destroy describe Admin::UsersController, "routing" do it "to #team_update" do - put("/admin/users/1/team_update").should route_to('admin/users#team_update', id: '1') + expect(put("/admin/users/1/team_update")).to route_to('admin/users#team_update', id: '1') end it "to #block" do - put("/admin/users/1/block").should route_to('admin/users#block', id: '1') + expect(put("/admin/users/1/block")).to route_to('admin/users#block', id: '1') end it "to #unblock" do - put("/admin/users/1/unblock").should route_to('admin/users#unblock', id: '1') + expect(put("/admin/users/1/unblock")).to route_to('admin/users#unblock', id: '1') end it "to #index" do - get("/admin/users").should route_to('admin/users#index') + expect(get("/admin/users")).to route_to('admin/users#index') end it "to #show" do - get("/admin/users/1").should route_to('admin/users#show', id: '1') + expect(get("/admin/users/1")).to route_to('admin/users#show', id: '1') end it "to #create" do - post("/admin/users").should route_to('admin/users#create') + expect(post("/admin/users")).to route_to('admin/users#create') end it "to #new" do - get("/admin/users/new").should route_to('admin/users#new') + expect(get("/admin/users/new")).to route_to('admin/users#new') end it "to #edit" do - get("/admin/users/1/edit").should route_to('admin/users#edit', id: '1') + expect(get("/admin/users/1/edit")).to route_to('admin/users#edit', id: '1') end it "to #show" do - get("/admin/users/1").should route_to('admin/users#show', id: '1') + expect(get("/admin/users/1")).to route_to('admin/users#show', id: '1') end it "to #update" do - put("/admin/users/1").should route_to('admin/users#update', id: '1') + expect(put("/admin/users/1")).to route_to('admin/users#update', id: '1') end it "to #destroy" do - delete("/admin/users/1").should route_to('admin/users#destroy', id: '1') + expect(delete("/admin/users/1")).to route_to('admin/users#destroy', id: '1') end end @@ -67,11 +67,11 @@ end # DELETE /admin/projects/:id(.:format) admin/projects#destroy {id: /[^\/]+/} describe Admin::ProjectsController, "routing" do it "to #index" do - get("/admin/projects").should route_to('admin/projects#index') + expect(get("/admin/projects")).to route_to('admin/projects#index') end it "to #show" do - get("/admin/projects/gitlab").should route_to('admin/projects#show', id: 'gitlab') + expect(get("/admin/projects/gitlab")).to route_to('admin/projects#show', namespace_id: 'gitlab') end end @@ -81,19 +81,19 @@ end # admin_hook DELETE /admin/hooks/:id(.:format) admin/hooks#destroy describe Admin::HooksController, "routing" do it "to #test" do - get("/admin/hooks/1/test").should route_to('admin/hooks#test', hook_id: '1') + expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', hook_id: '1') end it "to #index" do - get("/admin/hooks").should route_to('admin/hooks#index') + expect(get("/admin/hooks")).to route_to('admin/hooks#index') end it "to #create" do - post("/admin/hooks").should route_to('admin/hooks#create') + expect(post("/admin/hooks")).to route_to('admin/hooks#create') end it "to #destroy" do - delete("/admin/hooks/1").should route_to('admin/hooks#destroy', id: '1') + expect(delete("/admin/hooks/1")).to route_to('admin/hooks#destroy', id: '1') end end @@ -101,21 +101,21 @@ end # admin_logs GET /admin/logs(.:format) admin/logs#show describe Admin::LogsController, "routing" do it "to #show" do - get("/admin/logs").should route_to('admin/logs#show') + expect(get("/admin/logs")).to route_to('admin/logs#show') end end # admin_background_jobs GET /admin/background_jobs(.:format) admin/background_jobs#show describe Admin::BackgroundJobsController, "routing" do it "to #show" do - get("/admin/background_jobs").should route_to('admin/background_jobs#show') + expect(get("/admin/background_jobs")).to route_to('admin/background_jobs#show') end end # admin_root /admin(.:format) admin/dashboard#index describe Admin::DashboardController, "routing" do it "to #index" do - get("/admin").should route_to('admin/dashboard#index') + expect(get("/admin")).to route_to('admin/dashboard#index') end end diff --git a/spec/routing/notifications_routing_spec.rb b/spec/routing/notifications_routing_spec.rb index 112b825e023..24592942a96 100644 --- a/spec/routing/notifications_routing_spec.rb +++ b/spec/routing/notifications_routing_spec.rb @@ -3,11 +3,11 @@ require "spec_helper" describe Profiles::NotificationsController do describe "routing" do it "routes to #show" do - get("/profile/notifications").should route_to("profiles/notifications#show") + expect(get("/profile/notifications")).to route_to("profiles/notifications#show") end it "routes to #update" do - put("/profile/notifications").should route_to("profiles/notifications#update") + expect(put("/profile/notifications")).to route_to("profiles/notifications#update") end end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index ea584c9802d..0040718d9be 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -12,81 +12,88 @@ require 'spec_helper' # Examples # # # Default behavior -# it_behaves_like "RESTful project resources" do +# it_behaves_like 'RESTful project resources' do # let(:controller) { 'issues' } # end # # # Customizing actions -# it_behaves_like "RESTful project resources" do +# it_behaves_like 'RESTful project resources' do # let(:actions) { [:index] } # let(:controller) { 'issues' } # end -shared_examples "RESTful project resources" do +shared_examples 'RESTful project resources' do let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] } - it "to #index" do - get("/gitlab/gitlabhq/#{controller}").should route_to("projects/#{controller}#index", project_id: 'gitlab/gitlabhq') if actions.include?(:index) + it 'to #index' do + expect(get("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index) end - it "to #create" do - post("/gitlab/gitlabhq/#{controller}").should route_to("projects/#{controller}#create", project_id: 'gitlab/gitlabhq') if actions.include?(:create) + it 'to #create' do + expect(post("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create) end - it "to #new" do - get("/gitlab/gitlabhq/#{controller}/new").should route_to("projects/#{controller}#new", project_id: 'gitlab/gitlabhq') if actions.include?(:new) + it 'to #new' do + expect(get("/gitlab/gitlabhq/#{controller}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new) end - it "to #edit" do - get("/gitlab/gitlabhq/#{controller}/1/edit").should route_to("projects/#{controller}#edit", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:edit) + it 'to #edit' do + expect(get("/gitlab/gitlabhq/#{controller}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit) end - it "to #show" do - get("/gitlab/gitlabhq/#{controller}/1").should route_to("projects/#{controller}#show", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:show) + it 'to #show' do + expect(get("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show) end - it "to #update" do - put("/gitlab/gitlabhq/#{controller}/1").should route_to("projects/#{controller}#update", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:update) + it 'to #update' do + expect(put("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update) end - it "to #destroy" do - delete("/gitlab/gitlabhq/#{controller}/1").should route_to("projects/#{controller}#destroy", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:destroy) + it 'to #destroy' do + expect(delete("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy) end end -# projects POST /projects(.:format) projects#create -# new_project GET /projects/new(.:format) projects#new -# files_project GET /:id/files(.:format) projects#files -# edit_project GET /:id/edit(.:format) projects#edit -# project GET /:id(.:format) projects#show -# PUT /:id(.:format) projects#update -# DELETE /:id(.:format) projects#destroy -describe ProjectsController, "routing" do - it "to #create" do - post("/projects").should route_to('projects#create') +# projects POST /projects(.:format) projects#create +# new_project GET /projects/new(.:format) projects#new +# files_project GET /:id/files(.:format) projects#files +# edit_project GET /:id/edit(.:format) projects#edit +# project GET /:id(.:format) projects#show +# PUT /:id(.:format) projects#update +# DELETE /:id(.:format) projects#destroy +# markdown_preview_project POST /:id/markdown_preview(.:format) projects#markdown_preview +describe ProjectsController, 'routing' do + it 'to #create' do + expect(post('/projects')).to route_to('projects#create') end - it "to #new" do - get("/projects/new").should route_to('projects#new') + it 'to #new' do + expect(get('/projects/new')).to route_to('projects#new') end - it "to #edit" do - get("/gitlab/gitlabhq/edit").should route_to('projects#edit', id: 'gitlab/gitlabhq') + it 'to #edit' do + expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq') end - it "to #autocomplete_sources" do - get('/gitlab/gitlabhq/autocomplete_sources').should route_to('projects#autocomplete_sources', id: "gitlab/gitlabhq") + it 'to #autocomplete_sources' do + expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', namespace_id: 'gitlab', id: 'gitlabhq') end - it "to #show" do - get("/gitlab/gitlabhq").should route_to('projects#show', id: 'gitlab/gitlabhq') + it 'to #show' do + expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') end - it "to #update" do - put("/gitlab/gitlabhq").should route_to('projects#update', id: 'gitlab/gitlabhq') + it 'to #update' do + expect(put('/gitlab/gitlabhq')).to route_to('projects#update', namespace_id: 'gitlab', id: 'gitlabhq') end - it "to #destroy" do - delete("/gitlab/gitlabhq").should route_to('projects#destroy', id: 'gitlab/gitlabhq') + it 'to #destroy' do + expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', namespace_id: 'gitlab', id: 'gitlabhq') + end + + it 'to #markdown_preview' do + expect(post('/gitlab/gitlabhq/markdown_preview')).to( + route_to('projects#markdown_preview', namespace_id: 'gitlab', id: 'gitlabhq') + ) end end @@ -96,16 +103,16 @@ end # edit_project_wiki GET /:project_id/wikis/:id/edit(.:format) projects/wikis#edit # project_wiki GET /:project_id/wikis/:id(.:format) projects/wikis#show # DELETE /:project_id/wikis/:id(.:format) projects/wikis#destroy -describe Projects::WikisController, "routing" do - it "to #pages" do - get("/gitlab/gitlabhq/wikis/pages").should route_to('projects/wikis#pages', project_id: 'gitlab/gitlabhq') +describe Projects::WikisController, 'routing' do + it 'to #pages' do + expect(get('/gitlab/gitlabhq/wikis/pages')).to route_to('projects/wikis#pages', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it "to #history" do - get("/gitlab/gitlabhq/wikis/1/history").should route_to('projects/wikis#history', project_id: 'gitlab/gitlabhq', id: '1') + it 'to #history' do + expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end - it_behaves_like "RESTful project resources" do + it_behaves_like 'RESTful project resources' do let(:actions) { [:create, :edit, :show, :destroy] } let(:controller) { 'wikis' } end @@ -115,45 +122,45 @@ end # tags_project_repository GET /:project_id/repository/tags(.:format) projects/repositories#tags # archive_project_repository GET /:project_id/repository/archive(.:format) projects/repositories#archive # edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit -describe Projects::RepositoriesController, "routing" do - it "to #archive" do - get("/gitlab/gitlabhq/repository/archive").should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq') +describe Projects::RepositoriesController, 'routing' do + it 'to #archive' do + expect(get('/gitlab/gitlabhq/repository/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it "to #archive format:zip" do - get("/gitlab/gitlabhq/repository/archive.zip").should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq', format: 'zip') + it 'to #archive format:zip' do + expect(get('/gitlab/gitlabhq/repository/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip') end - it "to #archive format:tar.bz2" do - get("/gitlab/gitlabhq/repository/archive.tar.bz2").should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq', format: 'tar.bz2') + it 'to #archive format:tar.bz2' do + expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2') end - it "to #show" do - get("/gitlab/gitlabhq/repository").should route_to('projects/repositories#show', project_id: 'gitlab/gitlabhq') + it 'to #show' do + expect(get('/gitlab/gitlabhq/repository')).to route_to('projects/repositories#show', namespace_id: 'gitlab', project_id: 'gitlabhq') end end -describe Projects::BranchesController, "routing" do - it "to #branches" do - get("/gitlab/gitlabhq/branches").should route_to('projects/branches#index', project_id: 'gitlab/gitlabhq') - delete("/gitlab/gitlabhq/branches/feature%2345").should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45') - delete("/gitlab/gitlabhq/branches/feature%2B45").should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45') - delete("/gitlab/gitlabhq/branches/feature@45").should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45') - delete("/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz").should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45/foo/bar/baz') - delete("/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz").should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45/foo/bar/baz') - delete("/gitlab/gitlabhq/branches/feature@45/foo/bar/baz").should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45/foo/bar/baz') +describe Projects::BranchesController, 'routing' do + it 'to #branches' do + expect(get('/gitlab/gitlabhq/branches')).to route_to('projects/branches#index', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(delete('/gitlab/gitlabhq/branches/feature%2345')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45') + expect(delete('/gitlab/gitlabhq/branches/feature%2B45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45') + expect(delete('/gitlab/gitlabhq/branches/feature@45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45') + expect(delete('/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/branches/feature@45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz') end end -describe Projects::TagsController, "routing" do - it "to #tags" do - get("/gitlab/gitlabhq/tags").should route_to('projects/tags#index', project_id: 'gitlab/gitlabhq') - delete("/gitlab/gitlabhq/tags/feature%2345").should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45') - delete("/gitlab/gitlabhq/tags/feature%2B45").should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45') - delete("/gitlab/gitlabhq/tags/feature@45").should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45') - delete("/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz").should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45/foo/bar/baz') - delete("/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz").should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45/foo/bar/baz') - delete("/gitlab/gitlabhq/tags/feature@45/foo/bar/baz").should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45/foo/bar/baz') +describe Projects::TagsController, 'routing' do + it 'to #tags' do + expect(get('/gitlab/gitlabhq/tags')).to route_to('projects/tags#index', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(delete('/gitlab/gitlabhq/tags/feature%2345')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45') + expect(delete('/gitlab/gitlabhq/tags/feature%2B45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45') + expect(delete('/gitlab/gitlabhq/tags/feature@45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45') + expect(delete('/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/tags/feature@45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz') end end @@ -161,12 +168,11 @@ end # project_deploy_keys GET /:project_id/deploy_keys(.:format) deploy_keys#index # POST /:project_id/deploy_keys(.:format) deploy_keys#create # new_project_deploy_key GET /:project_id/deploy_keys/new(.:format) deploy_keys#new -# edit_project_deploy_key GET /:project_id/deploy_keys/:id/edit(.:format) deploy_keys#edit # project_deploy_key GET /:project_id/deploy_keys/:id(.:format) deploy_keys#show -# PUT /:project_id/deploy_keys/:id(.:format) deploy_keys#update # DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy -describe Projects::DeployKeysController, "routing" do - it_behaves_like "RESTful project resources" do +describe Projects::DeployKeysController, 'routing' do + it_behaves_like 'RESTful project resources' do + let(:actions) { [:index, :new, :create] } let(:controller) { 'deploy_keys' } end end @@ -174,8 +180,8 @@ end # project_protected_branches GET /:project_id/protected_branches(.:format) protected_branches#index # POST /:project_id/protected_branches(.:format) protected_branches#create # project_protected_branch DELETE /:project_id/protected_branches/:id(.:format) protected_branches#destroy -describe Projects::ProtectedBranchesController, "routing" do - it_behaves_like "RESTful project resources" do +describe Projects::ProtectedBranchesController, 'routing' do + it_behaves_like 'RESTful project resources' do let(:actions) { [:index, :create, :destroy] } let(:controller) { 'protected_branches' } end @@ -184,66 +190,74 @@ end # switch_project_refs GET /:project_id/refs/switch(.:format) refs#switch # logs_tree_project_ref GET /:project_id/refs/:id/logs_tree(.:format) refs#logs_tree # logs_file_project_ref GET /:project_id/refs/:id/logs_tree/:path(.:format) refs#logs_tree -describe Projects::RefsController, "routing" do - it "to #switch" do - get("/gitlab/gitlabhq/refs/switch").should route_to('projects/refs#switch', project_id: 'gitlab/gitlabhq') - end - - it "to #logs_tree" do - get("/gitlab/gitlabhq/refs/stable/logs_tree").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable') - get("/gitlab/gitlabhq/refs/feature%2345/logs_tree").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45') - get("/gitlab/gitlabhq/refs/feature%2B45/logs_tree").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45') - get("/gitlab/gitlabhq/refs/feature@45/logs_tree").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature@45') - get("/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'foo/bar/baz') - get("/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45', path: 'foo/bar/baz') - get("/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45', path: 'foo/bar/baz') - get("/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature@45', path: 'foo/bar/baz') - get("/gitlab/gitlabhq/refs/stable/logs_tree/files.scss").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'files.scss') +describe Projects::RefsController, 'routing' do + it 'to #switch' do + expect(get('/gitlab/gitlabhq/refs/switch')).to route_to('projects/refs#switch', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #logs_tree' do + expect(get('/gitlab/gitlabhq/refs/stable/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable') + expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45') + expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45') + expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45') + expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/files.scss')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'files.scss') end end -# diffs_project_merge_request GET /:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs -# automerge_project_merge_request POST /:project_id/merge_requests/:id/automerge(.:format) projects/merge_requests#automerge -# automerge_check_project_merge_request GET /:project_id/merge_requests/:id/automerge_check(.:format) projects/merge_requests#automerge_check -# branch_from_project_merge_requests GET /:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from -# branch_to_project_merge_requests GET /:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to -# project_merge_requests GET /:project_id/merge_requests(.:format) projects/merge_requests#index -# POST /:project_id/merge_requests(.:format) projects/merge_requests#create -# new_project_merge_request GET /:project_id/merge_requests/new(.:format) projects/merge_requests#new -# edit_project_merge_request GET /:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit -# project_merge_request GET /:project_id/merge_requests/:id(.:format) projects/merge_requests#show -# PUT /:project_id/merge_requests/:id(.:format) projects/merge_requests#update -# DELETE /:project_id/merge_requests/:id(.:format) projects/merge_requests#destroy -describe Projects::MergeRequestsController, "routing" do - it "to #diffs" do - get("/gitlab/gitlabhq/merge_requests/1/diffs").should route_to('projects/merge_requests#diffs', project_id: 'gitlab/gitlabhq', id: '1') - end - - it "to #automerge" do - post('/gitlab/gitlabhq/merge_requests/1/automerge').should route_to( +# diffs_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs +# commits_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/commits(.:format) projects/merge_requests#commits +# automerge_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/automerge(.:format) projects/merge_requests#automerge +# automerge_check_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/automerge_check(.:format) projects/merge_requests#automerge_check +# ci_status_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/ci_status(.:format) projects/merge_requests#ci_status +# toggle_subscription_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/toggle_subscription(.:format) projects/merge_requests#toggle_subscription +# branch_from_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from +# branch_to_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to +# update_branches_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/update_branches(.:format) projects/merge_requests#update_branches +# namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#index +# POST /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#create +# new_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/new(.:format) projects/merge_requests#new +# edit_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit +# namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#show +# PATCH /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update +# PUT /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update +describe Projects::MergeRequestsController, 'routing' do + it 'to #diffs' do + expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + + it 'to #commits' do + expect(get('/gitlab/gitlabhq/merge_requests/1/commits')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + + it 'to #automerge' do + expect(post('/gitlab/gitlabhq/merge_requests/1/automerge')).to route_to( 'projects/merge_requests#automerge', - project_id: 'gitlab/gitlabhq', id: '1' + namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1' ) end - it "to #automerge_check" do - get("/gitlab/gitlabhq/merge_requests/1/automerge_check").should route_to('projects/merge_requests#automerge_check', project_id: 'gitlab/gitlabhq', id: '1') + it 'to #automerge_check' do + expect(get('/gitlab/gitlabhq/merge_requests/1/automerge_check')).to route_to('projects/merge_requests#automerge_check', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end - it "to #branch_from" do - get("/gitlab/gitlabhq/merge_requests/branch_from").should route_to('projects/merge_requests#branch_from', project_id: 'gitlab/gitlabhq') + it 'to #branch_from' do + expect(get('/gitlab/gitlabhq/merge_requests/branch_from')).to route_to('projects/merge_requests#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it "to #branch_to" do - get("/gitlab/gitlabhq/merge_requests/branch_to").should route_to('projects/merge_requests#branch_to', project_id: 'gitlab/gitlabhq') + it 'to #branch_to' do + expect(get('/gitlab/gitlabhq/merge_requests/branch_to')).to route_to('projects/merge_requests#branch_to', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it "to #show" do - get("/gitlab/gitlabhq/merge_requests/1.diff").should route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'diff') - get("/gitlab/gitlabhq/merge_requests/1.patch").should route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'patch') + it 'to #show' do + expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff') + expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch') end - it_behaves_like "RESTful project resources" do + it_behaves_like 'RESTful project resources' do let(:controller) { 'merge_requests' } let(:actions) { [:index, :create, :new, :edit, :show, :update] } end @@ -257,37 +271,37 @@ end # project_snippet GET /:project_id/snippets/:id(.:format) snippets#show # PUT /:project_id/snippets/:id(.:format) snippets#update # DELETE /:project_id/snippets/:id(.:format) snippets#destroy -describe SnippetsController, "routing" do - it "to #raw" do - get("/gitlab/gitlabhq/snippets/1/raw").should route_to('projects/snippets#raw', project_id: 'gitlab/gitlabhq', id: '1') +describe SnippetsController, 'routing' do + it 'to #raw' do + expect(get('/gitlab/gitlabhq/snippets/1/raw')).to route_to('projects/snippets#raw', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end - it "to #index" do - get("/gitlab/gitlabhq/snippets").should route_to("projects/snippets#index", project_id: 'gitlab/gitlabhq') + it 'to #index' do + expect(get('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#index', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it "to #create" do - post("/gitlab/gitlabhq/snippets").should route_to("projects/snippets#create", project_id: 'gitlab/gitlabhq') + it 'to #create' do + expect(post('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#create', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it "to #new" do - get("/gitlab/gitlabhq/snippets/new").should route_to("projects/snippets#new", project_id: 'gitlab/gitlabhq') + it 'to #new' do + expect(get('/gitlab/gitlabhq/snippets/new')).to route_to('projects/snippets#new', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it "to #edit" do - get("/gitlab/gitlabhq/snippets/1/edit").should route_to("projects/snippets#edit", project_id: 'gitlab/gitlabhq', id: '1') + it 'to #edit' do + expect(get('/gitlab/gitlabhq/snippets/1/edit')).to route_to('projects/snippets#edit', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end - it "to #show" do - get("/gitlab/gitlabhq/snippets/1").should route_to("projects/snippets#show", project_id: 'gitlab/gitlabhq', id: '1') + it 'to #show' do + expect(get('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end - it "to #update" do - put("/gitlab/gitlabhq/snippets/1").should route_to("projects/snippets#update", project_id: 'gitlab/gitlabhq', id: '1') + it 'to #update' do + expect(put('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#update', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end - it "to #destroy" do - delete("/gitlab/gitlabhq/snippets/1").should route_to("projects/snippets#destroy", project_id: 'gitlab/gitlabhq', id: '1') + it 'to #destroy' do + expect(delete('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end end @@ -295,24 +309,24 @@ end # project_hooks GET /:project_id/hooks(.:format) hooks#index # POST /:project_id/hooks(.:format) hooks#create # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy -describe Projects::HooksController, "routing" do - it "to #test" do - get("/gitlab/gitlabhq/hooks/1/test").should route_to('projects/hooks#test', project_id: 'gitlab/gitlabhq', id: '1') +describe Projects::HooksController, 'routing' do + it 'to #test' do + expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end - it_behaves_like "RESTful project resources" do + it_behaves_like 'RESTful project resources' do let(:actions) { [:index, :create, :destroy] } let(:controller) { 'hooks' } end end # project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /[[:alnum:]]{6,40}/, project_id: /[^\/]+/} -describe Projects::CommitController, "routing" do - it "to #show" do - get("/gitlab/gitlabhq/commit/4246fb").should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb') - get("/gitlab/gitlabhq/commit/4246fb.diff").should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'diff') - get("/gitlab/gitlabhq/commit/4246fb.patch").should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'patch') - get("/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5").should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') +describe Projects::CommitController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/commit/4246fb')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb') + expect(get('/gitlab/gitlabhq/commit/4246fb.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb', format: 'diff') + expect(get('/gitlab/gitlabhq/commit/4246fb.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb', format: 'patch') + expect(get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') end end @@ -320,28 +334,25 @@ end # project_commits GET /:project_id/commits(.:format) commits#index # POST /:project_id/commits(.:format) commits#create # project_commit GET /:project_id/commits/:id(.:format) commits#show -describe Projects::CommitsController, "routing" do - it_behaves_like "RESTful project resources" do +describe Projects::CommitsController, 'routing' do + it_behaves_like 'RESTful project resources' do let(:actions) { [:show] } let(:controller) { 'commits' } end - it "to #show" do - get("/gitlab/gitlabhq/commits/master.atom").should route_to('projects/commits#show', project_id: 'gitlab/gitlabhq', id: "master", format: "atom") + it 'to #show' do + expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'atom') end end -# project_team_members GET /:project_id/team_members(.:format) team_members#index -# POST /:project_id/team_members(.:format) team_members#create -# new_project_team_member GET /:project_id/team_members/new(.:format) team_members#new -# edit_project_team_member GET /:project_id/team_members/:id/edit(.:format) team_members#edit -# project_team_member GET /:project_id/team_members/:id(.:format) team_members#show -# PUT /:project_id/team_members/:id(.:format) team_members#update -# DELETE /:project_id/team_members/:id(.:format) team_members#destroy -describe Projects::TeamMembersController, "routing" do - it_behaves_like "RESTful project resources" do - let(:actions) { [:new, :create, :update, :destroy] } - let(:controller) { 'team_members' } +# project_project_members GET /:project_id/project_members(.:format) project_members#index +# POST /:project_id/project_members(.:format) project_members#create +# PUT /:project_id/project_members/:id(.:format) project_members#update +# DELETE /:project_id/project_members/:id(.:format) project_members#destroy +describe Projects::ProjectMembersController, 'routing' do + it_behaves_like 'RESTful project resources' do + let(:actions) { [:index, :create, :update, :destroy] } + let(:controller) { 'project_members' } end end @@ -352,17 +363,17 @@ end # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show # PUT /:project_id/milestones/:id(.:format) milestones#update # DELETE /:project_id/milestones/:id(.:format) milestones#destroy -describe Projects::MilestonesController, "routing" do - it_behaves_like "RESTful project resources" do +describe Projects::MilestonesController, 'routing' do + it_behaves_like 'RESTful project resources' do let(:controller) { 'milestones' } let(:actions) { [:index, :create, :new, :edit, :show, :update] } end end # project_labels GET /:project_id/labels(.:format) labels#index -describe Projects::LabelsController, "routing" do - it "to #index" do - get("/gitlab/gitlabhq/labels").should route_to('projects/labels#index', project_id: 'gitlab/gitlabhq') +describe Projects::LabelsController, 'routing' do + it 'to #index' do + expect(get('/gitlab/gitlabhq/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq') end end @@ -376,94 +387,114 @@ end # project_issue GET /:project_id/issues/:id(.:format) issues#show # PUT /:project_id/issues/:id(.:format) issues#update # DELETE /:project_id/issues/:id(.:format) issues#destroy -describe Projects::IssuesController, "routing" do - it "to #bulk_update" do - post("/gitlab/gitlabhq/issues/bulk_update").should route_to('projects/issues#bulk_update', project_id: 'gitlab/gitlabhq') +describe Projects::IssuesController, 'routing' do + it 'to #bulk_update' do + expect(post('/gitlab/gitlabhq/issues/bulk_update')).to route_to('projects/issues#bulk_update', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it_behaves_like "RESTful project resources" do + it_behaves_like 'RESTful project resources' do let(:controller) { 'issues' } let(:actions) { [:index, :create, :new, :edit, :show, :update] } end end -# preview_project_notes POST /:project_id/notes/preview(.:format) notes#preview # project_notes GET /:project_id/notes(.:format) notes#index # POST /:project_id/notes(.:format) notes#create # project_note DELETE /:project_id/notes/:id(.:format) notes#destroy -describe Projects::NotesController, "routing" do - it "to #preview" do - post("/gitlab/gitlabhq/notes/preview").should route_to('projects/notes#preview', project_id: 'gitlab/gitlabhq') - end - - it_behaves_like "RESTful project resources" do +describe Projects::NotesController, 'routing' do + it_behaves_like 'RESTful project resources' do let(:actions) { [:index, :create, :destroy] } let(:controller) { 'notes' } end end # project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /.+/, project_id: /[^\/]+/} -describe Projects::BlameController, "routing" do - it "to #show" do - get("/gitlab/gitlabhq/blame/master/app/models/project.rb").should route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') - get("/gitlab/gitlabhq/blame/master/files.scss").should route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') +describe Projects::BlameController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/blame/master/app/models/project.rb')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') + expect(get('/gitlab/gitlabhq/blame/master/files.scss')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') end end # project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /.+/, project_id: /[^\/]+/} -describe Projects::BlobController, "routing" do - it "to #show" do - get("/gitlab/gitlabhq/blob/master/app/models/project.rb").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') - get("/gitlab/gitlabhq/blob/master/app/models/compare.rb").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/compare.rb') - get("/gitlab/gitlabhq/blob/master/files.scss").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') +describe Projects::BlobController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/blob/master/app/models/project.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') + expect(get('/gitlab/gitlabhq/blob/master/app/models/compare.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/compare.rb') + expect(get('/gitlab/gitlabhq/blob/master/app/models/diff.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/diff.js') + expect(get('/gitlab/gitlabhq/blob/master/files.scss')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') end end # project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /.+/, project_id: /[^\/]+/} -describe Projects::TreeController, "routing" do - it "to #show" do - get("/gitlab/gitlabhq/tree/master/app/models/project.rb").should route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') - get("/gitlab/gitlabhq/tree/master/files.scss").should route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') +describe Projects::TreeController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') + expect(get('/gitlab/gitlabhq/tree/master/files.scss')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') + end +end + +describe Projects::BlobController, 'routing' do + it 'to #edit' do + expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to( + route_to('projects/blob#edit', + namespace_id: 'gitlab', project_id: 'gitlabhq', + id: 'master/app/models/project.rb')) + end + + it 'to #preview' do + expect(post('/gitlab/gitlabhq/preview/master/app/models/project.rb')).to( + route_to('projects/blob#preview', + namespace_id: 'gitlab', project_id: 'gitlabhq', + id: 'master/app/models/project.rb')) end end # project_compare_index GET /:project_id/compare(.:format) compare#index {id: /[^\/]+/, project_id: /[^\/]+/} # POST /:project_id/compare(.:format) compare#create {id: /[^\/]+/, project_id: /[^\/]+/} # project_compare /:project_id/compare/:from...:to(.:format) compare#show {from: /.+/, to: /.+/, id: /[^\/]+/, project_id: /[^\/]+/} -describe Projects::CompareController, "routing" do - it "to #index" do - get("/gitlab/gitlabhq/compare").should route_to('projects/compare#index', project_id: 'gitlab/gitlabhq') +describe Projects::CompareController, 'routing' do + it 'to #index' do + expect(get('/gitlab/gitlabhq/compare')).to route_to('projects/compare#index', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it "to #compare" do - post("/gitlab/gitlabhq/compare").should route_to('projects/compare#create', project_id: 'gitlab/gitlabhq') + it 'to #compare' do + expect(post('/gitlab/gitlabhq/compare')).to route_to('projects/compare#create', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it "to #show" do - get("/gitlab/gitlabhq/compare/master...stable").should route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'master', to: 'stable') - get("/gitlab/gitlabhq/compare/issue/1234...stable").should route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'issue/1234', to: 'stable') + it 'to #show' do + expect(get('/gitlab/gitlabhq/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable') + expect(get('/gitlab/gitlabhq/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable') end end -describe Projects::NetworkController, "routing" do - it "to #show" do - get("/gitlab/gitlabhq/network/master").should route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master') - get("/gitlab/gitlabhq/network/master.json").should route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master', format: "json") +describe Projects::NetworkController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') + expect(get('/gitlab/gitlabhq/network/master.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') end end -describe Projects::GraphsController, "routing" do - it "to #show" do - get("/gitlab/gitlabhq/graphs/master").should route_to('projects/graphs#show', project_id: 'gitlab/gitlabhq', id: 'master') +describe Projects::GraphsController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') end end -describe Projects::ForksController, "routing" do - it "to #new" do - get("/gitlab/gitlabhq/fork/new").should route_to("projects/forks#new", project_id: 'gitlab/gitlabhq') +describe Projects::ForksController, 'routing' do + it 'to #new' do + expect(get('/gitlab/gitlabhq/fork/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it "to #create" do - post("/gitlab/gitlabhq/fork").should route_to("projects/forks#create", project_id: 'gitlab/gitlabhq') + it 'to #create' do + expect(post('/gitlab/gitlabhq/fork')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq') + end +end + +# project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy +describe Projects::AvatarsController, 'routing' do + it 'to #destroy' do + expect(delete('/gitlab/gitlabhq/avatar')).to route_to( + 'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq') end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 1e92cf62dd5..f268e4755d1 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' # search GET /search(.:format) search#show describe SearchController, "routing" do it "to #show" do - get("/search").should route_to('search#show') + expect(get("/search")).to route_to('search#show') end end @@ -11,11 +11,11 @@ end # /:path Grack describe "Mounted Apps", "routing" do it "to API" do - get("/api/issues").should be_routable + expect(get("/api/issues")).to be_routable end it "to Grack" do - get("/gitlab/gitlabhq.git").should be_routable + expect(get("/gitlab/gitlabhq.git")).to be_routable end end @@ -28,86 +28,71 @@ end # DELETE /snippets/:id(.:format) snippets#destroy describe SnippetsController, "routing" do it "to #user_index" do - get("/s/User").should route_to('snippets#user_index', username: 'User') + expect(get("/s/User")).to route_to('snippets#index', username: 'User') end it "to #raw" do - get("/snippets/1/raw").should route_to('snippets#raw', id: '1') + expect(get("/snippets/1/raw")).to route_to('snippets#raw', id: '1') end it "to #index" do - get("/snippets").should route_to('snippets#index') + expect(get("/snippets")).to route_to('snippets#index') end it "to #create" do - post("/snippets").should route_to('snippets#create') + expect(post("/snippets")).to route_to('snippets#create') end it "to #new" do - get("/snippets/new").should route_to('snippets#new') + expect(get("/snippets/new")).to route_to('snippets#new') end it "to #edit" do - get("/snippets/1/edit").should route_to('snippets#edit', id: '1') + expect(get("/snippets/1/edit")).to route_to('snippets#edit', id: '1') end it "to #show" do - get("/snippets/1").should route_to('snippets#show', id: '1') + expect(get("/snippets/1")).to route_to('snippets#show', id: '1') end it "to #update" do - put("/snippets/1").should route_to('snippets#update', id: '1') + expect(put("/snippets/1")).to route_to('snippets#update', id: '1') end it "to #destroy" do - delete("/snippets/1").should route_to('snippets#destroy', id: '1') + expect(delete("/snippets/1")).to route_to('snippets#destroy', id: '1') end end -# help GET /help(.:format) help#index -# help_permissions GET /help/permissions(.:format) help#permissions -# help_workflow GET /help/workflow(.:format) help#workflow -# help_api GET /help/api(.:format) help#api -# help_web_hooks GET /help/web_hooks(.:format) help#web_hooks -# help_system_hooks GET /help/system_hooks(.:format) help#system_hooks -# help_markdown GET /help/markdown(.:format) help#markdown -# help_ssh GET /help/ssh(.:format) help#ssh -# help_raketasks GET /help/raketasks(.:format) help#raketasks +# help GET /help(.:format) help#index +# help_page GET /help/:category/:file(.:format) help#show {:category=>/.*/, :file=>/[^\/\.]+/} +# help_shortcuts GET /help/shortcuts(.:format) help#shortcuts +# help_ui GET /help/ui(.:format) help#ui describe HelpController, "routing" do it "to #index" do - get("/help").should route_to('help#index') + expect(get("/help")).to route_to('help#index') end - it "to #permissions" do - get("/help/permissions/permissions").should route_to('help#show', category: "permissions", file: "permissions") - end - - it "to #workflow" do - get("/help/workflow/README").should route_to('help#show', category: "workflow", file: "README") - end - - it "to #api" do - get("/help/api/README").should route_to('help#show', category: "api", file: "README") - end - - it "to #web_hooks" do - get("/help/web_hooks/web_hooks").should route_to('help#show', category: "web_hooks", file: "web_hooks") - end - - it "to #system_hooks" do - get("/help/system_hooks/system_hooks").should route_to('help#show', category: "system_hooks", file: "system_hooks") - end + it 'to #show' do + path = '/help/markdown/markdown.md' + expect(get(path)).to route_to('help#show', + category: 'markdown', + file: 'markdown', + format: 'md') - it "to #markdown" do - get("/help/markdown/markdown").should route_to('help#show',category: "markdown", file: "markdown") + path = '/help/workflow/protected_branches/protected_branches1.png' + expect(get(path)).to route_to('help#show', + category: 'workflow/protected_branches', + file: 'protected_branches1', + format: 'png') end - it "to #ssh" do - get("/help/ssh/README").should route_to('help#show', category: "ssh", file: "README") + it 'to #shortcuts' do + expect(get('/help/shortcuts')).to route_to('help#shortcuts') end - it "to #raketasks" do - get("/help/raketasks/README").should route_to('help#show', category: "raketasks", file: "README") + it 'to #ui' do + expect(get('/help/ui')).to route_to('help#ui') end end @@ -117,27 +102,36 @@ end # profile_token GET /profile/token(.:format) profile#token # profile_reset_private_token PUT /profile/reset_private_token(.:format) profile#reset_private_token # profile GET /profile(.:format) profile#show -# profile_design GET /profile/design(.:format) profile#design # profile_update PUT /profile/update(.:format) profile#update describe ProfilesController, "routing" do it "to #account" do - get("/profile/account").should route_to('profiles/accounts#show') + expect(get("/profile/account")).to route_to('profiles/accounts#show') end it "to #history" do - get("/profile/history").should route_to('profiles#history') + expect(get("/profile/history")).to route_to('profiles#history') end it "to #reset_private_token" do - put("/profile/reset_private_token").should route_to('profiles#reset_private_token') + expect(put("/profile/reset_private_token")).to route_to('profiles#reset_private_token') end it "to #show" do - get("/profile").should route_to('profiles#show') + expect(get("/profile")).to route_to('profiles#show') + end +end + +# profile_preferences GET /profile/preferences(.:format) profiles/preferences#show +# PATCH /profile/preferences(.:format) profiles/preferences#update +# PUT /profile/preferences(.:format) profiles/preferences#update +describe Profiles::PreferencesController, 'routing' do + it 'to #show' do + expect(get('/profile/preferences')).to route_to('profiles/preferences#show') end - it "to #design" do - get("/profile/design").should route_to('profiles#design') + it 'to #update' do + expect(put('/profile/preferences')).to route_to('profiles/preferences#update') + expect(patch('/profile/preferences')).to route_to('profiles/preferences#update') end end @@ -150,36 +144,36 @@ end # DELETE /keys/:id(.:format) keys#destroy describe Profiles::KeysController, "routing" do it "to #index" do - get("/profile/keys").should route_to('profiles/keys#index') + expect(get("/profile/keys")).to route_to('profiles/keys#index') end it "to #create" do - post("/profile/keys").should route_to('profiles/keys#create') + expect(post("/profile/keys")).to route_to('profiles/keys#create') end it "to #new" do - get("/profile/keys/new").should route_to('profiles/keys#new') + expect(get("/profile/keys/new")).to route_to('profiles/keys#new') end it "to #edit" do - get("/profile/keys/1/edit").should route_to('profiles/keys#edit', id: '1') + expect(get("/profile/keys/1/edit")).to route_to('profiles/keys#edit', id: '1') end it "to #show" do - get("/profile/keys/1").should route_to('profiles/keys#show', id: '1') + expect(get("/profile/keys/1")).to route_to('profiles/keys#show', id: '1') end it "to #update" do - put("/profile/keys/1").should route_to('profiles/keys#update', id: '1') + expect(put("/profile/keys/1")).to route_to('profiles/keys#update', id: '1') end it "to #destroy" do - delete("/profile/keys/1").should route_to('profiles/keys#destroy', id: '1') + expect(delete("/profile/keys/1")).to route_to('profiles/keys#destroy', id: '1') end # get all the ssh-keys of a user it "to #get_keys" do - get("/foo.keys").should route_to('profiles/keys#get_keys', username: 'foo') + expect(get("/foo.keys")).to route_to('profiles/keys#get_keys', username: 'foo') end end @@ -188,44 +182,50 @@ end # DELETE /keys/:id(.:format) keys#destroy describe Profiles::EmailsController, "routing" do it "to #index" do - get("/profile/emails").should route_to('profiles/emails#index') + expect(get("/profile/emails")).to route_to('profiles/emails#index') end it "to #create" do - post("/profile/emails").should route_to('profiles/emails#create') + expect(post("/profile/emails")).to route_to('profiles/emails#create') end it "to #destroy" do - delete("/profile/emails/1").should route_to('profiles/emails#destroy', id: '1') + expect(delete("/profile/emails/1")).to route_to('profiles/emails#destroy', id: '1') end end # profile_avatar DELETE /profile/avatar(.:format) profiles/avatars#destroy describe Profiles::AvatarsController, "routing" do it "to #destroy" do - delete("/profile/avatar").should route_to('profiles/avatars#destroy') + expect(delete("/profile/avatar")).to route_to('profiles/avatars#destroy') end end # dashboard GET /dashboard(.:format) dashboard#show # dashboard_issues GET /dashboard/issues(.:format) dashboard#issues # dashboard_merge_requests GET /dashboard/merge_requests(.:format) dashboard#merge_requests -# root / dashboard#show describe DashboardController, "routing" do it "to #index" do - get("/dashboard").should route_to('dashboard#show') - get("/").should route_to('dashboard#show') + expect(get("/dashboard")).to route_to('dashboard#show') end it "to #issues" do - get("/dashboard/issues").should route_to('dashboard#issues') + expect(get("/dashboard/issues")).to route_to('dashboard#issues') end it "to #merge_requests" do - get("/dashboard/merge_requests").should route_to('dashboard#merge_requests') + expect(get("/dashboard/merge_requests")).to route_to('dashboard#merge_requests') end end +# root / root#show +describe RootController, 'routing' do + it 'to #show' do + expect(get('/')).to route_to('root#show') + end +end + + # new_user_session GET /users/sign_in(.:format) devise/sessions#new # user_session POST /users/sign_in(.:format) devise/sessions#create # destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy @@ -241,11 +241,11 @@ end describe "Groups", "routing" do it "to #show" do - get("/groups/1").should route_to('groups#show', id: '1') + expect(get("/groups/1")).to route_to('groups#show', id: '1') end it "also display group#show on the short path" do - get('/1').should route_to('namespaces#show', id: '1') + expect(get('/1')).to route_to('namespaces#show', id: '1') end end diff --git a/spec/services/archive_repository_service_spec.rb b/spec/services/archive_repository_service_spec.rb new file mode 100644 index 00000000000..c22426fccdb --- /dev/null +++ b/spec/services/archive_repository_service_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe ArchiveRepositoryService do + let(:project) { create(:project) } + subject { ArchiveRepositoryService.new(project, "master", "zip") } + + describe "#execute" do + it "cleans old archives" do + expect(project.repository).to receive(:clean_old_archives) + + subject.execute(timeout: 0.0) + end + + context "when the repository doesn't have an archive file path" do + before do + allow(project.repository).to receive(:archive_file_path).and_return(nil) + end + + it "raises an error" do + expect { + subject.execute(timeout: 0.0) + }.to raise_error(RuntimeError) + end + end + + context "when the repository has an archive file path" do + let(:file_path) { "/archive.zip" } + let(:pid_file_path) { "/archive.zip.pid" } + + before do + allow(project.repository).to receive(:archive_file_path).and_return(file_path) + allow(project.repository).to receive(:archive_pid_file_path).and_return(pid_file_path) + end + + context "when the archive file already exists" do + before do + allow(File).to receive(:exist?).with(file_path).and_return(true) + end + + it "returns the file path" do + expect(subject.execute(timeout: 0.0)).to eq(file_path) + end + end + + context "when the archive file doesn't exist yet" do + before do + allow(File).to receive(:exist?).with(file_path).and_return(false) + allow(File).to receive(:exist?).with(pid_file_path).and_return(true) + end + + context "when the archive pid file doesn't exist yet" do + before do + allow(File).to receive(:exist?).with(pid_file_path).and_return(false) + end + + it "queues the RepositoryArchiveWorker" do + expect(RepositoryArchiveWorker).to receive(:perform_async) + + subject.execute(timeout: 0.0) + end + end + + context "when the archive pid file already exists" do + it "doesn't queue the RepositoryArchiveWorker" do + expect(RepositoryArchiveWorker).not_to receive(:perform_async) + + subject.execute(timeout: 0.0) + end + end + + context "when the archive file exists after a little while" do + before do + Thread.new do + sleep 0.1 + allow(File).to receive(:exist?).with(file_path).and_return(true) + end + end + + it "returns the file path" do + expect(subject.execute(timeout: 0.2)).to eq(file_path) + end + end + + context "when the archive file doesn't exist after the timeout" do + it "returns nil" do + expect(subject.execute(timeout: 0.0)).to eq(nil) + end + end + end + end + end +end + diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb new file mode 100644 index 00000000000..08689c15ca8 --- /dev/null +++ b/spec/services/create_snippet_service_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe CreateSnippetService do + before do + @user = create :user + @admin = create :user, admin: true + @opts = { + title: 'Test snippet', + file_name: 'snippet.rb', + content: 'puts "hello world"', + visibility_level: Gitlab::VisibilityLevel::PRIVATE + } + end + + context 'When public visibility is restricted' do + before do + allow_any_instance_of(ApplicationSetting).to( + receive(:restricted_visibility_levels).and_return( + [Gitlab::VisibilityLevel::PUBLIC] + ) + ) + + @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + + it 'non-admins should not be able to create a public snippet' do + snippet = create_snippet(nil, @user, @opts) + expect(snippet.errors.messages).to have_key(:visibility_level) + expect(snippet.errors.messages[:visibility_level].first).to( + match('Public visibility has been restricted') + ) + end + + it 'admins should be able to create a public snippet' do + snippet = create_snippet(nil, @admin, @opts) + expect(snippet.errors.any?).to be_falsey + expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + + def create_snippet(project, user, opts) + CreateSnippetService.new(project, user, opts).execute + end +end diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb new file mode 100644 index 00000000000..e28564b3866 --- /dev/null +++ b/spec/services/destroy_group_service_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe DestroyGroupService do + let!(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:project) { create(:project, namespace: group) } + let!(:gitlab_shell) { Gitlab::Shell.new } + let!(:remove_path) { group.path + "+#{group.id}+deleted" } + + context 'database records' do + before do + destroy_group(group, user) + end + + it { expect(Group.all).not_to include(group) } + it { expect(Project.all).not_to include(project) } + end + + context 'file system' do + context 'Sidekiq inline' do + before do + # Run sidekiq immediatly to check that renamed dir will be removed + Sidekiq::Testing.inline! { destroy_group(group, user) } + end + + it { expect(gitlab_shell.exists?(group.path)).to be_falsey } + it { expect(gitlab_shell.exists?(remove_path)).to be_falsey } + end + + context 'Sidekiq fake' do + before do + # Dont run sidekiq to check if renamed repository exists + Sidekiq::Testing.fake! { destroy_group(group, user) } + end + + it { expect(gitlab_shell.exists?(group.path)).to be_falsey } + it { expect(gitlab_shell.exists?(remove_path)).to be_truthy } + end + end + + def destroy_group(group, user) + DestroyGroupService.new(group, user).execute + end +end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index 713aa3e7e74..007a9eed192 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -7,7 +7,7 @@ describe EventCreateService do describe :open_issue do let(:issue) { create(:issue) } - it { service.open_issue(issue, issue.author).should be_true } + it { expect(service.open_issue(issue, issue.author)).to be_truthy } it "should create new event" do expect { service.open_issue(issue, issue.author) }.to change { Event.count } @@ -17,7 +17,7 @@ describe EventCreateService do describe :close_issue do let(:issue) { create(:issue) } - it { service.close_issue(issue, issue.author).should be_true } + it { expect(service.close_issue(issue, issue.author)).to be_truthy } it "should create new event" do expect { service.close_issue(issue, issue.author) }.to change { Event.count } @@ -27,7 +27,7 @@ describe EventCreateService do describe :reopen_issue do let(:issue) { create(:issue) } - it { service.reopen_issue(issue, issue.author).should be_true } + it { expect(service.reopen_issue(issue, issue.author)).to be_truthy } it "should create new event" do expect { service.reopen_issue(issue, issue.author) }.to change { Event.count } @@ -39,7 +39,7 @@ describe EventCreateService do describe :open_mr do let(:merge_request) { create(:merge_request) } - it { service.open_mr(merge_request, merge_request.author).should be_true } + it { expect(service.open_mr(merge_request, merge_request.author)).to be_truthy } it "should create new event" do expect { service.open_mr(merge_request, merge_request.author) }.to change { Event.count } @@ -49,7 +49,7 @@ describe EventCreateService do describe :close_mr do let(:merge_request) { create(:merge_request) } - it { service.close_mr(merge_request, merge_request.author).should be_true } + it { expect(service.close_mr(merge_request, merge_request.author)).to be_truthy } it "should create new event" do expect { service.close_mr(merge_request, merge_request.author) }.to change { Event.count } @@ -59,7 +59,7 @@ describe EventCreateService do describe :merge_mr do let(:merge_request) { create(:merge_request) } - it { service.merge_mr(merge_request, merge_request.author).should be_true } + it { expect(service.merge_mr(merge_request, merge_request.author)).to be_truthy } it "should create new event" do expect { service.merge_mr(merge_request, merge_request.author) }.to change { Event.count } @@ -69,7 +69,7 @@ describe EventCreateService do describe :reopen_mr do let(:merge_request) { create(:merge_request) } - it { service.reopen_mr(merge_request, merge_request.author).should be_true } + it { expect(service.reopen_mr(merge_request, merge_request.author)).to be_truthy } it "should create new event" do expect { service.reopen_mr(merge_request, merge_request.author) }.to change { Event.count } @@ -83,7 +83,7 @@ describe EventCreateService do describe :open_milestone do let(:milestone) { create(:milestone) } - it { service.open_milestone(milestone, user).should be_true } + it { expect(service.open_milestone(milestone, user)).to be_truthy } it "should create new event" do expect { service.open_milestone(milestone, user) }.to change { Event.count } @@ -93,7 +93,7 @@ describe EventCreateService do describe :close_mr do let(:milestone) { create(:milestone) } - it { service.close_milestone(milestone, user).should be_true } + it { expect(service.close_milestone(milestone, user)).to be_truthy } it "should create new event" do expect { service.close_milestone(milestone, user) }.to change { Event.count } diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 19b442573f4..435b14eb245 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -20,7 +20,7 @@ describe GitPushService do service.execute(project, user, @blankrev, @newrev, @ref) end - it { should be_true } + it { is_expected.to be_truthy } end context 'existing branch' do @@ -28,7 +28,7 @@ describe GitPushService do service.execute(project, user, @oldrev, @newrev, @ref) end - it { should be_true } + it { is_expected.to be_truthy } end context 'rm branch' do @@ -36,7 +36,7 @@ describe GitPushService do service.execute(project, user, @oldrev, @blankrev, @ref) end - it { should be_true } + it { is_expected.to be_truthy } end end @@ -44,46 +44,59 @@ describe GitPushService do before do service.execute(project, user, @oldrev, @newrev, @ref) @push_data = service.push_data - @commit = project.repository.commit(@newrev) + @commit = project.commit(@newrev) end subject { @push_data } - it { should include(before: @oldrev) } - it { should include(after: @newrev) } - it { should include(ref: @ref) } - it { should include(user_id: user.id) } - it { should include(user_name: user.name) } - it { should include(project_id: project.id) } + it { is_expected.to include(object_kind: 'push') } + it { is_expected.to include(before: @oldrev) } + it { is_expected.to include(after: @newrev) } + it { is_expected.to include(ref: @ref) } + it { is_expected.to include(user_id: user.id) } + it { is_expected.to include(user_name: user.name) } + it { is_expected.to include(project_id: project.id) } context "with repository data" do subject { @push_data[:repository] } - it { should include(name: project.name) } - it { should include(url: project.url_to_repo) } - it { should include(description: project.description) } - it { should include(homepage: project.web_url) } + it { is_expected.to include(name: project.name) } + it { is_expected.to include(url: project.url_to_repo) } + it { is_expected.to include(description: project.description) } + it { is_expected.to include(homepage: project.web_url) } end context "with commits" do subject { @push_data[:commits] } - it { should be_an(Array) } - it { should have(1).element } + it { is_expected.to be_an(Array) } + it 'has 1 element' do + expect(subject.size).to eq(1) + end context "the commit" do subject { @push_data[:commits].first } - it { should include(id: @commit.id) } - it { should include(message: @commit.safe_message) } - it { should include(timestamp: @commit.date.xmlschema) } - it { should include(url: "#{Gitlab.config.gitlab.url}/#{project.to_param}/commit/#{@commit.id}") } + it { is_expected.to include(id: @commit.id) } + it { is_expected.to include(message: @commit.safe_message) } + it { is_expected.to include(timestamp: @commit.date.xmlschema) } + it do + is_expected.to include( + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + @commit.id + ].join('/') + ) + end context "with a author" do subject { @push_data[:commits].first[:author] } - it { should include(name: @commit.author_name) } - it { should include(email: @commit.author_email) } + it { is_expected.to include(name: @commit.author_name) } + it { is_expected.to include(email: @commit.author_email) } end end end @@ -95,29 +108,46 @@ describe GitPushService do @event = Event.last end - it { @event.should_not be_nil } - it { @event.project.should == project } - it { @event.action.should == Event::PUSHED } - it { @event.data.should == service.push_data } + it { expect(@event).not_to be_nil } + it { expect(@event.project).to eq(project) } + it { expect(@event.action).to eq(Event::PUSHED) } + it { expect(@event.data).to eq(service.push_data) } end describe "Web Hooks" do context "execute web hooks" do it "when pushing a branch for the first time" do - project.should_receive(:execute_hooks) - project.default_branch.should == "master" - project.protected_branches.should_receive(:create).with({ name: "master" }) + expect(project).to receive(:execute_hooks) + expect(project.default_branch).to eq("master") + expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false }) service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') end - it "when pushing new commits to existing branch" do - project.should_receive(:execute_hooks) - service.execute(project, user, 'oldrev', 'newrev', 'refs/heads/master') + it "when pushing a branch for the first time with default branch protection disabled" do + allow(ApplicationSetting.current_application_settings). + to receive(:default_branch_protection). + and_return(Gitlab::Access::PROTECTION_NONE) + + expect(project).to receive(:execute_hooks) + expect(project.default_branch).to eq("master") + expect(project.protected_branches).not_to receive(:create) + service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') + end + + it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do + allow(ApplicationSetting.current_application_settings). + to receive(:default_branch_protection). + and_return(Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + expect(project).to receive(:execute_hooks) + expect(project.default_branch).to eq("master") + expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: true }) + service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') end - it "when pushing tags" do - project.should_not_receive(:execute_hooks) - service.execute(project, user, 'newrev', 'newrev', 'refs/tags/v1.0.0') + it "when pushing new commits to existing branch" do + expect(project).to receive(:execute_hooks) + service.execute(project, user, 'oldrev', 'newrev', 'refs/heads/master') end end end @@ -125,79 +155,74 @@ describe GitPushService do describe "cross-reference notes" do let(:issue) { create :issue, project: project } let(:commit_author) { create :user } - let(:commit) { project.repository.commit } + let(:commit) { project.commit } before do - commit.stub({ + allow(commit).to receive_messages( safe_message: "this commit \n mentions ##{issue.id}", references: [issue], author_name: commit_author.name, author_email: commit_author.email - }) - project.repository.stub(commits_between: [commit]) + ) + allow(project.repository).to receive(:commits_between).and_return([commit]) end it "creates a note if a pushed commit mentions an issue" do - Note.should_receive(:create_cross_reference_note).with(issue, commit, commit_author, project) + expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author) service.execute(project, user, @oldrev, @newrev, @ref) end it "only creates a cross-reference note if one doesn't already exist" do - Note.create_cross_reference_note(issue, commit, user, project) + SystemNoteService.cross_reference(issue, commit, user) - Note.should_not_receive(:create_cross_reference_note).with(issue, commit, commit_author, project) + expect(SystemNoteService).not_to receive(:cross_reference).with(issue, commit, commit_author) service.execute(project, user, @oldrev, @newrev, @ref) end it "defaults to the pushing user if the commit's author is not known" do - commit.stub(author_name: 'unknown name', author_email: 'unknown@email.com') - Note.should_receive(:create_cross_reference_note).with(issue, commit, user, project) + allow(commit).to receive_messages( + author_name: 'unknown name', + author_email: 'unknown@email.com' + ) + expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, user) service.execute(project, user, @oldrev, @newrev, @ref) end it "finds references in the first push to a non-default branch" do - project.repository.stub(:commits_between).with(@blankrev, @newrev).and_return([]) - project.repository.stub(:commits_between).with("master", @newrev).and_return([commit]) + allow(project.repository).to receive(:commits_between).with(@blankrev, @newrev).and_return([]) + allow(project.repository).to receive(:commits_between).with("master", @newrev).and_return([commit]) - Note.should_receive(:create_cross_reference_note).with(issue, commit, commit_author, project) + expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author) service.execute(project, user, @blankrev, @newrev, 'refs/heads/other') end - - it "finds references in the first push to a default branch" do - project.repository.stub(:commits_between).with(@blankrev, @newrev).and_return([]) - project.repository.stub(:commits).with(@newrev).and_return([commit]) - - Note.should_receive(:create_cross_reference_note).with(issue, commit, commit_author, project) - - service.execute(project, user, @blankrev, @newrev, 'refs/heads/master') - end end describe "closing issues from pushed commits" do let(:issue) { create :issue, project: project } let(:other_issue) { create :issue, project: project } let(:commit_author) { create :user } - let(:closing_commit) { project.repository.commit } + let(:closing_commit) { project.commit } before do - closing_commit.stub({ + allow(closing_commit).to receive_messages( issue_closing_regex: /^([Cc]loses|[Ff]ixes) #\d+/, safe_message: "this is some work.\n\ncloses ##{issue.iid}", author_name: commit_author.name, author_email: commit_author.email - }) + ) - project.repository.stub(commits_between: [closing_commit]) + allow(project.repository).to receive(:commits_between). + and_return([closing_commit]) end it "closes issues with commit messages" do service.execute(project, user, @oldrev, @newrev, @ref) - Issue.find(issue.id).should be_closed + expect(Issue.find(issue.id)).to be_closed end it "doesn't create cross-reference notes for a closing reference" do @@ -207,15 +232,37 @@ describe GitPushService do end it "doesn't close issues when pushed to non-default branches" do - project.stub(default_branch: 'durf') + allow(project).to receive(:default_branch).and_return('durf') # The push still shouldn't create cross-reference notes. expect { service.execute(project, user, @oldrev, @newrev, 'refs/heads/hurf') }.not_to change { Note.where(project_id: project.id, system: true).count } - Issue.find(issue.id).should be_opened + expect(Issue.find(issue.id)).to be_opened + end + + it "doesn't close issues when external issue tracker is in use" do + allow(project).to receive(:default_issues_tracker?).and_return(false) + + # The push still shouldn't create cross-reference notes. + expect { + service.execute(project, user, @oldrev, @newrev, 'refs/heads/hurf') + }.not_to change { Note.where(project_id: project.id, system: true).count } end end -end + describe "empty project" do + let(:project) { create(:project_empty_repo) } + let(:new_ref) { 'refs/heads/feature'} + + before do + allow(project).to receive(:default_branch).and_return('feature') + expect(project).to receive(:change_head) { 'feature'} + end + + it 'push to first branch updates HEAD' do + service.execute(project, user, @blankrev, @newrev, new_ref) + end + end +end diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index e65a8204c54..76f69b396e0 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -1,45 +1,87 @@ require 'spec_helper' describe GitTagPushService do + include RepoHelpers + let (:user) { create :user } let (:project) { create :project } let (:service) { GitTagPushService.new } before do - @ref = 'refs/tags/super-tag' - @oldrev = 'b98a310def241a6fd9c9a9a3e7934c48e498fe81' - @newrev = 'b19a04f53caeebf4fe5ec2327cb83e9253dc91bb' + @oldrev = Gitlab::Git::BLANK_SHA + @newrev = "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" # gitlab-test: git rev-parse refs/tags/v1.1.0 + @ref = 'refs/tags/v1.1.0' end - describe 'Git Tag Push Data' do + describe "Git Tag Push Data" do before do service.execute(project, user, @oldrev, @newrev, @ref) @push_data = service.push_data + @tag_name = Gitlab::Git.ref_name(@ref) + @tag = project.repository.find_tag(@tag_name) + @commit = project.commit(@tag.target) end subject { @push_data } - it { should include(ref: @ref) } - it { should include(before: @oldrev) } - it { should include(after: @newrev) } - it { should include(user_id: user.id) } - it { should include(user_name: user.name) } - it { should include(project_id: project.id) } + it { is_expected.to include(object_kind: 'tag_push') } + it { is_expected.to include(ref: @ref) } + it { is_expected.to include(before: @oldrev) } + it { is_expected.to include(after: @newrev) } + it { is_expected.to include(message: @tag.message) } + it { is_expected.to include(user_id: user.id) } + it { is_expected.to include(user_name: user.name) } + it { is_expected.to include(project_id: project.id) } - context 'With repository data' do + context "with repository data" do subject { @push_data[:repository] } - it { should include(name: project.name) } - it { should include(url: project.url_to_repo) } - it { should include(description: project.description) } - it { should include(homepage: project.web_url) } + it { is_expected.to include(name: project.name) } + it { is_expected.to include(url: project.url_to_repo) } + it { is_expected.to include(description: project.description) } + it { is_expected.to include(homepage: project.web_url) } + end + + context "with commits" do + subject { @push_data[:commits] } + + it { is_expected.to be_an(Array) } + it 'has 1 element' do + expect(subject.size).to eq(1) + end + + context "the commit" do + subject { @push_data[:commits].first } + + it { is_expected.to include(id: @commit.id) } + it { is_expected.to include(message: @commit.safe_message) } + it { is_expected.to include(timestamp: @commit.date.xmlschema) } + it do + is_expected.to include( + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + @commit.id + ].join('/') + ) + end + + context "with a author" do + subject { @push_data[:commits].first[:author] } + + it { is_expected.to include(name: @commit.author_name) } + it { is_expected.to include(email: @commit.author_email) } + end + end end end describe "Web Hooks" do context "execute web hooks" do it "when pushing tags" do - project.should_receive(:execute_hooks) + expect(project).to receive(:execute_hooks) service.execute(project, user, 'oldrev', 'newrev', 'refs/tags/v1.0.0') end end diff --git a/spec/services/issues/bulk_update_context_spec.rb b/spec/services/issues/bulk_update_context_spec.rb deleted file mode 100644 index f4c9148f1a3..00000000000 --- a/spec/services/issues/bulk_update_context_spec.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'spec_helper' - -describe Issues::BulkUpdateService 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 - - describe :close_issue do - - before do - @issues = 5.times.collect do - create(:issue, project: @project) - end - @params = { - update: { - status: 'closed', - issues_ids: @issues.map(&:id) - } - } - end - - it { - result = Issues::BulkUpdateService.new(@project, @user, @params).execute - result[:success].should be_true - result[:count].should == @issues.count - - @project.issues.opened.should be_empty - @project.issues.closed.should_not be_empty - } - - end - - describe :reopen_issues do - - before do - @issues = 5.times.collect do - create(:closed_issue, project: @project) - end - @params = { - update: { - status: 'reopen', - issues_ids: @issues.map(&:id) - } - } - end - - it { - result = Issues::BulkUpdateService.new(@project, @user, @params).execute - result[:success].should be_true - result[:count].should == @issues.count - - @project.issues.closed.should be_empty - @project.issues.opened.should_not be_empty - } - - end - - describe :update_assignee do - - before do - @new_assignee = create :user - @params = { - update: { - issues_ids: [issue.id], - assignee_id: @new_assignee.id - } - } - end - - it { - result = Issues::BulkUpdateService.new(@project, @user, @params).execute - result[:success].should be_true - result[:count].should == 1 - - @project.issues.first.assignee.should == @new_assignee - } - - end - - describe :update_milestone do - - before do - @milestone = create :milestone - @params = { - update: { - issues_ids: [issue.id], - milestone_id: @milestone.id - } - } - end - - it { - result = Issues::BulkUpdateService.new(@project, @user, @params).execute - result[:success].should be_true - result[:count].should == 1 - - @project.issues.first.milestone.should == @milestone - } - end - -end diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb new file mode 100644 index 00000000000..a97c55011c9 --- /dev/null +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -0,0 +1,121 @@ +require 'spec_helper' + +describe Issues::BulkUpdateService 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 + + describe :close_issue do + + before do + @issues = 5.times.collect do + create(:issue, project: @project) + end + @params = { + state_event: 'close', + issues_ids: @issues.map(&:id) + } + end + + it { + result = Issues::BulkUpdateService.new(@project, @user, @params).execute + 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 + } + + end + + describe :reopen_issues do + + before do + @issues = 5.times.collect do + create(:closed_issue, project: @project) + end + @params = { + state_event: 'reopen', + issues_ids: @issues.map(&:id) + } + end + + it { + result = Issues::BulkUpdateService.new(@project, @user, @params).execute + 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 + } + + end + + describe :update_assignee do + + before do + @new_assignee = create :user + @params = { + issues_ids: [issue.id], + assignee_id: @new_assignee.id + } + end + + it { + result = Issues::BulkUpdateService.new(@project, @user, @params).execute + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) + + expect(@project.issues.first.assignee).to eq(@new_assignee) + } + + it 'allows mass-unassigning' do + @project.issues.first.update_attribute(:assignee, @new_assignee) + expect(@project.issues.first.assignee).not_to be_nil + + @params[:assignee_id] = -1 + + Issues::BulkUpdateService.new(@project, @user, @params).execute + expect(@project.issues.first.assignee).to be_nil + 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] = '' + + Issues::BulkUpdateService.new(@project, @user, @params).execute + expect(@project.issues.first.assignee).not_to be_nil + end + end + + describe :update_milestone do + + before do + @milestone = create :milestone + @params = { + issues_ids: [issue.id], + milestone_id: @milestone.id + } + end + + it { + result = Issues::BulkUpdateService.new(@project, @user, @params).execute + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) + + expect(@project.issues.first.milestone).to eq(@milestone) + } + end + +end diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index d4f2cc1339b..db547ce0d50 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' describe Issues::CloseService do - let(:project) { create(:empty_project) } let(:user) { create(:user) } let(:user2) { create(:user) } let(:issue) { create(:issue, assignee: user2) } + let(:project) { issue.project } before do project.team << [user, :master] @@ -17,19 +17,29 @@ describe Issues::CloseService do @issue = Issues::CloseService.new(project, user, {}).execute(issue) end - it { @issue.should be_valid } - it { @issue.should be_closed } + it { expect(@issue).to be_valid } + it { expect(@issue).to be_closed } it 'should send email to user2 about assign of new issue' do email = ActionMailer::Base.deliveries.last - email.to.first.should == user2.email - email.subject.should include(issue.title) + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(issue.title) end it 'should create system note about issue reassign' do note = @issue.notes.last - note.note.should include "Status changed to closed" + expect(note.note).to include "Status changed to closed" end end + + context "external issue tracker" do + before do + allow(project).to receive(:default_issues_tracker?).and_return(false) + @issue = Issues::CloseService.new(project, user, {}).execute(issue) + end + + it { expect(@issue).to be_valid } + it { expect(@issue).to be_opened } + end end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 90720be5ded..7f1ebcb3198 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -16,8 +16,8 @@ describe Issues::CreateService do @issue = Issues::CreateService.new(project, user, opts).execute end - it { @issue.should be_valid } - it { @issue.title.should == 'Awesome issue' } + it { expect(@issue).to be_valid } + it { expect(@issue.title).to eq('Awesome issue') } end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 347560414e7..a91be3b4472 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1,43 +1,70 @@ require 'spec_helper' describe Issues::UpdateService do - let(:project) { create(:empty_project) } let(:user) { create(:user) } let(:user2) { create(:user) } - let(:issue) { create(:issue) } + let(:issue) { create(:issue, title: 'Old title') } + let(:label) { create(:label) } + let(:project) { issue.project } before do project.team << [user, :master] project.team << [user2, :developer] end - describe :execute do + describe 'execute' do context "valid params" do before do opts = { title: 'New title', description: 'Also please fix', assignee_id: user2.id, - state_event: 'close' + state_event: 'close', + label_ids: [label.id] } @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + @issue.reload end - it { @issue.should be_valid } - it { @issue.title.should == 'New title' } - it { @issue.assignee.should == user2 } - it { @issue.should be_closed } + it { expect(@issue).to be_valid } + it { expect(@issue.title).to eq('New title') } + it { expect(@issue.assignee).to eq(user2) } + it { expect(@issue).to be_closed } + it { expect(@issue.labels.count).to eq(1) } + it { expect(@issue.labels.first.title).to eq('Bug') } it 'should send email to user2 about assign of new issue' do email = ActionMailer::Base.deliveries.last - email.to.first.should == user2.email - email.subject.should include(issue.title) + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(issue.title) + end + + def find_note(starting_with) + @issue.notes.find do |note| + note && note.note.start_with?(starting_with) + end end it 'should create system note about issue reassign' do - note = @issue.notes.last - note.note.should include "Reassigned to \@#{user2.username}" + note = find_note('Reassigned to') + + expect(note).not_to be_nil + expect(note.note).to include "Reassigned to \@#{user2.username}" + end + + it 'should create system note about issue label edit' do + note = find_note('Added ~') + + expect(note).not_to be_nil + expect(note.note).to include "Added ~#{label.id} label" + end + + it 'creates system note about title change' do + note = find_note('Title changed') + + expect(note).not_to be_nil + expect(note.note).to eq 'Title changed from **Old title** to **New title**' end end end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index a504f916b08..b3cbfd4b5b8 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -12,23 +12,32 @@ describe MergeRequests::CloseService do end describe :execute do - context "valid params" do + context 'valid params' do + let(:service) { MergeRequests::CloseService.new(project, user, {}) } + before do - @merge_request = MergeRequests::CloseService.new(project, user, {}).execute(merge_request) + allow(service).to receive(:execute_hooks) + + @merge_request = service.execute(merge_request) end - it { @merge_request.should be_valid } - it { @merge_request.should be_closed } + it { expect(@merge_request).to be_valid } + it { expect(@merge_request).to be_closed } + + it 'should execute hooks with close action' do + expect(service).to have_received(:execute_hooks). + with(@merge_request, 'close') + end it 'should send email to user2 about assign of new merge_request' do email = ActionMailer::Base.deliveries.last - email.to.first.should == user2.email - email.subject.should include(merge_request.title) + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(merge_request.title) end it 'should create system note about merge_request reassign' do note = @merge_request.notes.last - note.note.should include "Status changed to closed" + expect(note.note).to include 'Status changed to closed' end end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index cebeb0644d0..d9bfdf64308 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -5,21 +5,30 @@ describe MergeRequests::CreateService do let(:user) { create(:user) } describe :execute do - context "valid params" do - before do - project.team << [user, :master] - opts = { + context 'valid params' do + let(:opts) do + { title: 'Awesome merge_request', description: 'please fix', source_branch: 'stable', target_branch: 'master' } + end + let(:service) { MergeRequests::CreateService.new(project, user, opts) } + + before do + project.team << [user, :master] + allow(service).to receive(:execute_hooks) - @merge_request = MergeRequests::CreateService.new(project, user, opts).execute + @merge_request = service.execute end - it { @merge_request.should be_valid } - it { @merge_request.title.should == 'Awesome merge_request' } + it { expect(@merge_request).to be_valid } + it { expect(@merge_request.title).to eq('Awesome merge_request') } + + it 'should execute hooks with default action' do + expect(service).to have_received(:execute_hooks).with(@merge_request) + end end end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb new file mode 100644 index 00000000000..0a25fb12f4e --- /dev/null +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe MergeRequests::MergeService do + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:merge_request) { create(:merge_request, assignee: user2) } + let(:project) { merge_request.project } + + before do + project.team << [user, :master] + project.team << [user2, :developer] + end + + describe :execute do + context 'valid params' do + let(:service) { MergeRequests::MergeService.new(project, user, {}) } + + before do + allow(service).to receive(:execute_hooks) + + service.execute(merge_request, 'Awesome message') + end + + it { expect(merge_request).to be_valid } + it { expect(merge_request).to be_merged } + + it 'should execute hooks with merge action' do + expect(service).to have_received(:execute_hooks). + with(merge_request, 'merge') + end + + it 'should send email to user2 about merge of new merge_request' do + email = ActionMailer::Base.deliveries.last + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(merge_request.title) + end + + it 'should create system note about merge_request merge' do + note = merge_request.notes.last + expect(note.note).to include 'Status changed to merged' + end + end + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 9f294152053..0f9b65678df 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -30,15 +30,22 @@ describe MergeRequests::RefreshService do end context 'push to origin repo source branch' do + let(:refresh_service) { service.new(@project, @user) } before do - service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') reload_mrs end - it { @merge_request.notes.should_not be_empty } - it { @merge_request.should be_open } - it { @fork_merge_request.should be_open } - it { @fork_merge_request.notes.should be_empty } + it 'should execute hooks with update action' do + expect(refresh_service).to have_received(:execute_hooks). + with(@merge_request, 'update') + end + + it { expect(@merge_request.notes).not_to be_empty } + it { expect(@merge_request).to be_open } + it { expect(@fork_merge_request).to be_open } + it { expect(@fork_merge_request.notes).to be_empty } end context 'push to origin repo target branch' do @@ -47,22 +54,29 @@ describe MergeRequests::RefreshService do reload_mrs end - it { @merge_request.notes.should be_empty } - it { @merge_request.should be_merged } - it { @fork_merge_request.should be_merged } - it { @fork_merge_request.notes.should be_empty } + it { expect(@merge_request.notes.last.note).to include('changed to merged') } + 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') } end context 'push to fork repo source branch' do + let(:refresh_service) { service.new(@fork_project, @user) } before do - service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/master') + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') reload_mrs end - it { @merge_request.notes.should be_empty } - it { @merge_request.should be_open } - it { @fork_merge_request.notes.should_not be_empty } - it { @fork_merge_request.should be_open } + it 'should execute hooks with update action' do + expect(refresh_service).to have_received(:execute_hooks). + with(@fork_merge_request, 'update') + end + + it { expect(@merge_request.notes).to be_empty } + 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 } end context 'push to fork repo target branch' do @@ -71,10 +85,10 @@ describe MergeRequests::RefreshService do reload_mrs end - it { @merge_request.notes.should be_empty } - it { @merge_request.should be_open } - it { @fork_merge_request.notes.should be_empty } - it { @fork_merge_request.should be_open } + it { expect(@merge_request.notes).to be_empty } + it { expect(@merge_request).to be_open } + it { expect(@fork_merge_request.notes).to be_empty } + it { expect(@fork_merge_request).to be_open } end context 'push to origin repo target branch after fork project was removed' do @@ -84,10 +98,10 @@ describe MergeRequests::RefreshService do reload_mrs end - it { @merge_request.notes.should be_empty } - it { @merge_request.should be_merged } - it { @fork_merge_request.should be_open } - it { @fork_merge_request.notes.should be_empty } + it { expect(@merge_request.notes.last.note).to include('changed to merged') } + it { expect(@merge_request).to be_merged } + it { expect(@fork_merge_request).to be_open } + it { expect(@fork_merge_request.notes).to be_empty } end def reload_mrs diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb new file mode 100644 index 00000000000..9401bc3b558 --- /dev/null +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe MergeRequests::ReopenService do + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:merge_request) { create(:merge_request, assignee: user2) } + let(:project) { merge_request.project } + + before do + project.team << [user, :master] + project.team << [user2, :developer] + end + + describe :execute do + context 'valid params' do + let(:service) { MergeRequests::ReopenService.new(project, user, {}) } + + before do + allow(service).to receive(:execute_hooks) + + merge_request.state = :closed + service.execute(merge_request) + end + + it { expect(merge_request).to be_valid } + it { expect(merge_request).to be_reopened } + + it 'should execute hooks with reopen action' do + expect(service).to have_received(:execute_hooks). + with(merge_request, 'reopen') + end + + it 'should send email to user2 about reopen of merge_request' do + email = ActionMailer::Base.deliveries.last + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(merge_request.title) + end + + it 'should create system note about merge_request reopen' do + note = merge_request.notes.last + expect(note.note).to include 'Status changed to reopened' + end + end + end +end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index af5d3a3dc81..c75173c1452 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -3,41 +3,88 @@ require 'spec_helper' describe MergeRequests::UpdateService do let(:user) { create(:user) } let(:user2) { create(:user) } - let(:merge_request) { create(:merge_request, :simple) } + let(:merge_request) { create(:merge_request, :simple, title: 'Old title') } let(:project) { merge_request.project } + let(:label) { create(:label) } before do project.team << [user, :master] project.team << [user2, :developer] end - describe :execute do - context "valid params" do - before do - opts = { + describe 'execute' do + context 'valid params' do + let(:opts) do + { title: 'New title', description: 'Also please fix', assignee_id: user2.id, - state_event: 'close' + state_event: 'close', + label_ids: [label.id], + target_branch: 'target' } + end + + let(:service) { MergeRequests::UpdateService.new(project, user, opts) } + + before do + allow(service).to receive(:execute_hooks) - @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + @merge_request = service.execute(merge_request) + @merge_request.reload end - it { @merge_request.should be_valid } - it { @merge_request.title.should == 'New title' } - it { @merge_request.assignee.should == user2 } - it { @merge_request.should be_closed } + it { expect(@merge_request).to be_valid } + it { expect(@merge_request.title).to eq('New title') } + it { expect(@merge_request.assignee).to eq(user2) } + it { expect(@merge_request).to be_closed } + it { expect(@merge_request.labels.count).to eq(1) } + it { expect(@merge_request.labels.first.title).to eq('Bug') } + it { expect(@merge_request.target_branch).to eq('target') } + + it 'should execute hooks with update action' do + expect(service).to have_received(:execute_hooks). + with(@merge_request, 'update') + end it 'should send email to user2 about assign of new merge_request' do email = ActionMailer::Base.deliveries.last - email.to.first.should == user2.email - email.subject.should include(merge_request.title) + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(merge_request.title) + end + + def find_note(starting_with) + @merge_request.notes.find do |note| + note && note.note.start_with?(starting_with) + end end it 'should create system note about merge_request reassign' do - note = @merge_request.notes.last - note.note.should include "Reassigned to \@#{user2.username}" + note = find_note('Reassigned to') + + expect(note).not_to be_nil + expect(note.note).to include "Reassigned to \@#{user2.username}" + end + + it 'should create system note about merge_request label edit' do + note = find_note('Added ~') + + expect(note).not_to be_nil + expect(note.note).to include "Added ~#{label.id} label" + end + + it 'creates system note about title change' do + note = find_note('Title changed') + + expect(note).not_to be_nil + expect(note.note).to eq 'Title changed from **Old title** to **New title**' + end + + it 'creates system note about branch change' do + note = find_note('Target') + + expect(note).not_to be_nil + expect(note.note).to eq 'Target branch changed from `master` to `target`' end end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index f59786efcf9..0dc3b412783 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -15,11 +15,13 @@ describe Notes::CreateService do noteable_id: issue.id } + expect(project).to receive(:execute_hooks) + expect(project).to receive(:execute_services) @note = Notes::CreateService.new(project, user, opts).execute end - it { @note.should be_valid } - it { @note.note.should == 'Awesome comment' } + it { expect(@note).to be_valid } + it { expect(@note.note).to eq('Awesome comment') } end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index f8377650e0a..253e5823499 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -7,10 +7,10 @@ describe NotificationService do describe :new_key do let!(:key) { create(:personal_key) } - it { notification.new_key(key).should be_true } + it { expect(notification.new_key(key)).to be_truthy } it 'should sent email to key owner' do - Notify.should_receive(:new_ssh_key_email).with(key.id) + expect(Notify).to receive(:new_ssh_key_email).with(key.id) notification.new_key(key) end end @@ -20,10 +20,10 @@ describe NotificationService do describe :new_email do let!(:email) { create(:email) } - it { notification.new_email(email).should be_true } + it { expect(notification.new_email(email)).to be_truthy } it 'should send email to email owner' do - Notify.should_receive(:new_email_email).with(email.id) + expect(Notify).to receive(:new_email_email).with(email.id) notification.new_email(email) end end @@ -31,7 +31,8 @@ describe NotificationService do describe 'Notes' do context 'issue note' do - let(:issue) { create(:issue, assignee: create(:user)) } + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project, assignee: create(:user)) } let(:mentioned_issue) { create(:issue, assignee: issue.assignee) } let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced') } @@ -41,20 +42,25 @@ describe NotificationService do describe :new_note do it do + add_users_with_subscription(note.project, issue) + should_email(@u_watcher.id) should_email(note.noteable.author_id) should_email(note.noteable.assignee_id) should_email(@u_mentioned.id) + should_email(@subscriber.id) should_not_email(note.author_id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) + should_not_email(@unsubscriber.id) + notification.new_note(note) end it 'filters out "mentioned in" notes' do - mentioned_note = Note.create_cross_reference_note(mentioned_issue, issue, issue.author, issue.project) + mentioned_note = SystemNoteService.cross_reference(mentioned_issue, issue, issue.author) - Notify.should_not_receive(:note_issue_email) + expect(Notify).not_to receive(:note_issue_email) notification.new_note(mentioned_note) end end @@ -69,9 +75,9 @@ describe NotificationService do user_project = note.project.project_members.find_by_user_id(@u_watcher.id) user_project.notification_level = Notification::N_PARTICIPATING user_project.save - user_group = note.project.group.group_members.find_by_user_id(@u_watcher.id) - user_group.notification_level = Notification::N_GLOBAL - user_group.save + group_member = note.project.group.group_members.find_by_user_id(@u_watcher.id) + group_member.notification_level = Notification::N_GLOBAL + group_member.save end it do @@ -87,16 +93,17 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:note_issue_email).with(user_id, note.id) + expect(Notify).to receive(:note_issue_email).with(user_id, note.id) end def should_not_email(user_id) - Notify.should_not_receive(:note_issue_email).with(user_id, note.id) + expect(Notify).not_to receive(:note_issue_email).with(user_id, note.id) end end context 'issue note mention' do - let(:issue) { create(:issue, assignee: create(:user)) } + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project, assignee: create(:user)) } let(:mentioned_issue) { create(:issue, assignee: issue.assignee) } let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@all mentioned') } @@ -116,34 +123,36 @@ describe NotificationService do should_email(note.noteable.assignee_id) should_not_email(note.author_id) + should_not_email(@u_mentioned.id) should_not_email(@u_disabled.id) should_not_email(@u_not_mentioned.id) notification.new_note(note) end it 'filters out "mentioned in" notes' do - mentioned_note = Note.create_cross_reference_note(mentioned_issue, issue, issue.author, issue.project) + mentioned_note = SystemNoteService.cross_reference(mentioned_issue, issue, issue.author) - Notify.should_not_receive(:note_issue_email) + expect(Notify).not_to receive(:note_issue_email) notification.new_note(mentioned_note) end end def should_email(user_id) - Notify.should_receive(:note_issue_email).with(user_id, note.id) + expect(Notify).to receive(:note_issue_email).with(user_id, note.id) end def should_not_email(user_id) - Notify.should_not_receive(:note_issue_email).with(user_id, note.id) + expect(Notify).not_to receive(:note_issue_email).with(user_id, note.id) end end context 'commit note' do - let(:note) { create(:note_on_commit) } + let(:project) { create(:project, :public) } + let(:note) { create(:note_on_commit, project: project) } before do build_team(note.project) - note.stub(:commit_author => @u_committer) + allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer) end describe :new_note do @@ -168,39 +177,55 @@ describe NotificationService do notification.new_note(note) end + it do + @u_committer.update_attributes(notification_level: Notification::N_MENTION) + should_not_email(@u_committer.id, note) + notification.new_note(note) + end + def should_email(user_id, n) - Notify.should_receive(:note_commit_email).with(user_id, n.id) + expect(Notify).to receive(:note_commit_email).with(user_id, n.id) end def should_not_email(user_id, n) - Notify.should_not_receive(:note_commit_email).with(user_id, n.id) + expect(Notify).not_to receive(:note_commit_email).with(user_id, n.id) end end end end describe 'Issues' do - let(:issue) { create :issue, assignee: create(:user) } + let(:project) { create(:empty_project, :public) } + let(:issue) { create :issue, project: project, assignee: create(:user), description: 'cc @participant' } before do build_team(issue.project) + add_users_with_subscription(issue.project, issue) end describe :new_issue do it do should_email(issue.assignee_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) + should_not_email(@u_mentioned.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) notification.new_issue(issue, @u_disabled) end + it do + issue.assignee.update_attributes(notification_level: Notification::N_MENTION) + should_not_email(issue.assignee_id) + notification.new_issue(issue, @u_disabled) + end + def should_email(user_id) - Notify.should_receive(:new_issue_email).with(user_id, issue.id) + expect(Notify).to receive(:new_issue_email).with(user_id, issue.id) end def should_not_email(user_id) - Notify.should_not_receive(:new_issue_email).with(user_id, issue.id) + expect(Notify).not_to receive(:new_issue_email).with(user_id, issue.id) end end @@ -208,6 +233,9 @@ describe NotificationService do it 'should email new assignee' do should_email(issue.assignee_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) @@ -215,11 +243,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:reassigned_issue_email).with(user_id, issue.id, nil, @u_disabled.id) + expect(Notify).to receive(:reassigned_issue_email).with(user_id, issue.id, nil, @u_disabled.id) end def should_not_email(user_id) - Notify.should_not_receive(:reassigned_issue_email).with(user_id, issue.id, issue.assignee_id, @u_disabled.id) + expect(Notify).not_to receive(:reassigned_issue_email).with(user_id, issue.id, issue.assignee_id, @u_disabled.id) end end @@ -228,6 +256,9 @@ describe NotificationService do should_email(issue.assignee_id) should_email(issue.author_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) @@ -235,11 +266,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id) + expect(Notify).to receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id) end def should_not_email(user_id) - Notify.should_not_receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id) + expect(Notify).not_to receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id) end end @@ -248,6 +279,9 @@ describe NotificationService do should_email(issue.assignee_id) should_email(issue.author_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) @@ -255,20 +289,22 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id) + expect(Notify).to receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id) end def should_not_email(user_id) - Notify.should_not_receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id) + expect(Notify).not_to receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id) end end end describe 'Merge Requests' do - let(:merge_request) { create :merge_request, assignee: create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user) } before do build_team(merge_request.target_project) + add_users_with_subscription(merge_request.target_project, merge_request) end describe :new_merge_request do @@ -281,11 +317,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:new_merge_request_email).with(user_id, merge_request.id) + expect(Notify).to receive(:new_merge_request_email).with(user_id, merge_request.id) end def should_not_email(user_id) - Notify.should_not_receive(:new_merge_request_email).with(user_id, merge_request.id) + expect(Notify).not_to receive(:new_merge_request_email).with(user_id, merge_request.id) end end @@ -293,17 +329,19 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) notification.reassigned_merge_request(merge_request, merge_request.author) end def should_email(user_id) - Notify.should_receive(:reassigned_merge_request_email).with(user_id, merge_request.id, nil, merge_request.author_id) + expect(Notify).to receive(:reassigned_merge_request_email).with(user_id, merge_request.id, nil, merge_request.author_id) end def should_not_email(user_id) - Notify.should_not_receive(:reassigned_merge_request_email).with(user_id, merge_request.id, merge_request.assignee_id, merge_request.author_id) + expect(Notify).not_to receive(:reassigned_merge_request_email).with(user_id, merge_request.id, merge_request.assignee_id, merge_request.author_id) end end @@ -311,17 +349,19 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) notification.close_mr(merge_request, @u_disabled) end def should_email(user_id) - Notify.should_receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) + expect(Notify).to receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) end def should_not_email(user_id) - Notify.should_not_receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) + expect(Notify).not_to receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) end end @@ -329,17 +369,19 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) notification.merge_mr(merge_request, @u_disabled) end def should_email(user_id) - Notify.should_receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) + expect(Notify).to receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) end def should_not_email(user_id) - Notify.should_not_receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) + expect(Notify).not_to receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) end end @@ -347,17 +389,19 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) notification.reopen_mr(merge_request, @u_disabled) end def should_email(user_id) - Notify.should_receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id) + expect(Notify).to receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id) end def should_not_email(user_id) - Notify.should_not_receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id) + expect(Notify).not_to receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id) end end end @@ -378,11 +422,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:project_was_moved_email).with(project.id, user_id) + expect(Notify).to receive(:project_was_moved_email).with(project.id, user_id) end def should_not_email(user_id) - Notify.should_not_receive(:project_was_moved_email).with(project.id, user_id) + expect(Notify).not_to receive(:project_was_moved_email).with(project.id, user_id) end end end @@ -390,8 +434,9 @@ describe NotificationService do def build_team(project) @u_watcher = create(:user, notification_level: Notification::N_WATCH) @u_participating = create(:user, notification_level: Notification::N_PARTICIPATING) + @u_participant_mentioned = create(:user, username: 'participant', notification_level: Notification::N_PARTICIPATING) @u_disabled = create(:user, notification_level: Notification::N_DISABLED) - @u_mentioned = create(:user, username: 'mention', notification_level: Notification::N_PARTICIPATING) + @u_mentioned = create(:user, username: 'mention', notification_level: Notification::N_MENTION) @u_committer = create(:user, username: 'committer') @u_not_mentioned = create(:user, username: 'regular', notification_level: Notification::N_PARTICIPATING) @@ -401,4 +446,15 @@ describe NotificationService do project.team << [@u_mentioned, :master] project.team << [@u_committer, :master] end + + def add_users_with_subscription(project, issuable) + @subscriber = create :user + @unsubscriber = create :user + + project.team << [@subscriber, :master] + project.team << [@unsubscriber, :master] + + issuable.subscriptions.create(user: @subscriber, subscribed: true) + issuable.subscriptions.create(user: @unsubscriber, subscribed: false) + end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 9c97dad2ff0..337dae592dd 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -16,9 +16,9 @@ describe Projects::CreateService do @project = create_project(@user, @opts) end - it { @project.should be_valid } - it { @project.owner.should == @user } - it { @project.namespace.should == @user.namespace } + it { expect(@project).to be_valid } + it { expect(@project.owner).to eq(@user) } + it { expect(@project.namespace).to eq(@user.namespace) } end context 'group namespace' do @@ -30,9 +30,9 @@ describe Projects::CreateService do @project = create_project(@user, @opts) end - it { @project.should be_valid } - it { @project.owner.should == @group } - it { @project.namespace.should == @group } + it { expect(@project).to be_valid } + it { expect(@project.owner).to eq(@group) } + it { expect(@project.namespace).to eq(@group) } end context 'wiki_enabled creates repository directory' do @@ -42,7 +42,7 @@ describe Projects::CreateService do @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end - it { File.exists?(@path).should be_true } + it { expect(File.exists?(@path)).to be_truthy } end context 'wiki_enabled false does not create wiki repository directory' do @@ -52,7 +52,34 @@ describe Projects::CreateService do @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end - it { File.exists?(@path).should be_false } + it { expect(File.exists?(@path)).to be_falsey } + end + end + + context 'restricted visibility level' do + before do + allow_any_instance_of(ApplicationSetting).to( + receive(:restricted_visibility_levels).and_return([20]) + ) + + @opts.merge!( + visibility_level: Gitlab::VisibilityLevel.options['Public'] + ) + end + + it 'should not allow a restricted visibility level for non-admins' do + project = create_project(@user, @opts) + expect(project).to respond_to(:errors) + expect(project.errors.messages).to have_key(:visibility_level) + expect(project.errors.messages[:visibility_level].first).to( + match('restricted by your GitLab administrator') + ) + end + + it 'should allow a restricted visibility level for admins' do + project = create_project(@admin, @opts) + expect(project.errors.any?).to be(false) + expect(project.saved?).to be(true) end end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb new file mode 100644 index 00000000000..e83eef0b1a2 --- /dev/null +++ b/spec/services/projects/destroy_service_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Projects::DestroyService do + let!(:user) { create(:user) } + let!(:project) { create(:project, namespace: user.namespace) } + let!(:path) { project.repository.path_to_repo } + let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") } + + context 'Sidekiq inline' do + before do + # Run sidekiq immediatly to check that renamed repository will be removed + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + 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 } + end + + context 'Sidekiq fake' do + before do + # Dont run sidekiq to check if renamed repository exists + Sidekiq::Testing.fake! { destroy_project(project, user, {}) } + 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 } + end + + def destroy_project(project, user, params) + Projects::DestroyService.new(project, user, params).execute + end +end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 5c80345c2b3..f158ac87e2b 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -16,18 +16,18 @@ describe Projects::ForkService do describe "successfully creates project in the user namespace" do let(:to_project) { fork_project(@from_project, @to_user) } - it { to_project.owner.should == @to_user } - it { to_project.namespace.should == @to_user.namespace } - it { to_project.star_count.should be_zero } - it { to_project.description.should == @from_project.description } + it { expect(to_project.owner).to eq(@to_user) } + it { expect(to_project.namespace).to eq(@to_user.namespace) } + it { expect(to_project.star_count).to be_zero } + it { expect(to_project.description).to eq(@from_project.description) } end end context 'fork project failure' do it "fails due to transaction failure" do @to_project = fork_project(@from_project, @to_user, false) - @to_project.errors.should_not be_empty - @to_project.errors[:base].should include("Fork transaction failed.") + expect(@to_project.errors).not_to be_empty + expect(@to_project.errors[:base]).to include("Failed to fork repository") end end @@ -35,9 +35,20 @@ describe Projects::ForkService do it "should fail due to validation, not transaction failure" do @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) @to_project = fork_project(@from_project, @to_user) - @existing_project.persisted?.should be_true - @to_project.errors[:base].should include("Invalid fork destination") - @to_project.errors[:base].should_not include("Fork transaction failed.") + expect(@existing_project.persisted?).to be_truthy + expect(@to_project.errors[:name]).to eq(['has already been taken']) + expect(@to_project.errors[:path]).to eq(['has already been taken']) + end + end + + context 'GitLab CI is enabled' do + it "calls fork registrator for CI" do + @from_project.build_missing_services + @from_project.gitlab_ci_service.update_attributes(active: true) + + expect(ForkRegistrationWorker).to receive(:perform_async) + + fork_project(@from_project, @to_user) end end end @@ -58,19 +69,19 @@ describe Projects::ForkService do context 'fork project for group' do it 'group owner successfully forks project into the group' do to_project = fork_project(@project, @group_owner, true, @opts) - to_project.owner.should == @group - to_project.namespace.should == @group - to_project.name.should == @project.name - to_project.path.should == @project.path - to_project.description.should == @project.description - to_project.star_count.should be_zero + expect(to_project.owner).to eq(@group) + expect(to_project.namespace).to eq(@group) + expect(to_project.name).to eq(@project.name) + expect(to_project.path).to eq(@project.path) + expect(to_project.description).to eq(@project.description) + expect(to_project.star_count).to be_zero end end context 'fork project for group when user not owner' do it 'group developer should fail to fork project into the group' do to_project = fork_project(@project, @developer, true, @opts) - to_project.errors[:namespace].should == ['insufficient access rights'] + expect(to_project.errors[:namespace]).to eq(['is not valid']) end end @@ -79,18 +90,15 @@ describe Projects::ForkService do existing_project = create(:project, name: @project.name, namespace: @group) to_project = fork_project(@project, @group_owner, true, @opts) - existing_project.persisted?.should be_true - to_project.errors[:base].should == ['Invalid fork destination'] - to_project.errors[:name].should == ['has already been taken'] - to_project.errors[:path].should == ['has already been taken'] + expect(existing_project.persisted?).to be_truthy + expect(to_project.errors[:name]).to eq(['has already been taken']) + expect(to_project.errors[:path]).to eq(['has already been taken']) end end end def fork_project(from_project, user, fork_success = true, params = {}) - context = Projects::ForkService.new(from_project, user, params) - shell = double('gitlab_shell').stub(fork_repository: fork_success) - context.stub(gitlab_shell: shell) - context.execute + allow_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(fork_success) + Projects::ForkService.new(from_project, user, params).execute end end diff --git a/spec/services/projects/image_service_spec.rb b/spec/services/projects/image_service_spec.rb deleted file mode 100644 index 23c4e227ae3..00000000000 --- a/spec/services/projects/image_service_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'spec_helper' - -describe Projects::ImageService do - describe 'Image service' do - before do - @user = create :user - @project = create :project, creator_id: @user.id, namespace: @user.namespace - end - - context 'for valid gif file' do - before do - gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') - @link_to_image = upload_image(@project.repository, { 'markdown_img' => gif }, "http://test.example/") - end - - it { expect(@link_to_image).to have_key("alt") } - it { expect(@link_to_image).to have_key("url") } - it { expect(@link_to_image).to have_value("banana_sample") } - it { expect(@link_to_image["url"]).to match("http://test.example/uploads/#{@project.path_with_namespace}") } - it { expect(@link_to_image["url"]).to match("banana_sample.gif") } - end - - context 'for valid png file' do - before do - png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png') - @link_to_image = upload_image(@project.repository, { 'markdown_img' => png }, "http://test.example/") - end - - it { expect(@link_to_image).to have_key("alt") } - it { expect(@link_to_image).to have_key("url") } - it { expect(@link_to_image).to have_value("dk") } - it { expect(@link_to_image["url"]).to match("http://test.example/uploads/#{@project.path_with_namespace}") } - it { expect(@link_to_image["url"]).to match("dk.png") } - end - - context 'for valid jpg file' do - before do - jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') - @link_to_image = upload_image(@project.repository, { 'markdown_img' => jpg }, "http://test.example/") - end - - it { expect(@link_to_image).to have_key("alt") } - it { expect(@link_to_image).to have_key("url") } - it { expect(@link_to_image).to have_value("rails_sample") } - it { expect(@link_to_image["url"]).to match("http://test.example/uploads/#{@project.path_with_namespace}") } - it { expect(@link_to_image["url"]).to match("rails_sample.jpg") } - end - - context 'for txt file' do - before do - txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') - @link_to_image = upload_image(@project.repository, { 'markdown_img' => txt }, "http://test.example/") - end - - it { expect(@link_to_image).to be_nil } - end - end - - def upload_image(repository, params, root_url) - Projects::ImageService.new(repository, params, root_url).execute - end -end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 79d0526ff89..79acba78bda 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -8,31 +8,29 @@ describe Projects::TransferService do context 'namespace -> namespace' do before do group.add_owner(user) - @result = transfer_project(project, user, namespace_id: group.id) + @result = transfer_project(project, user, new_namespace_id: group.id) end - it { @result.should be_true } - it { project.namespace.should == group } + it { expect(@result).to be_truthy } + it { expect(project.namespace).to eq(group) } end context 'namespace -> no namespace' do before do - @result = transfer_project(project, user, namespace_id: nil) + @result = transfer_project(project, user, new_namespace_id: nil) end - it { @result.should_not be_nil } # { result.should be_false } passes on nil - it { @result.should be_false } - it { project.namespace.should == user.namespace } + it { expect(@result).to eq false } + it { expect(project.namespace).to eq(user.namespace) } end context 'namespace -> not allowed namespace' do before do - @result = transfer_project(project, user, namespace_id: group.id) + @result = transfer_project(project, user, new_namespace_id: group.id) end - it { @result.should_not be_nil } # { result.should be_false } passes on nil - it { @result.should be_false } - it { project.namespace.should == user.namespace } + it { expect(@result).to eq false } + it { expect(project.namespace).to eq(user.namespace) } end def transfer_project(project, user, params) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 5a10174eb36..ea5b8813105 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -17,8 +17,8 @@ describe Projects::UpdateService do update_project(@project, @user, @opts) end - it { @created_private.should be_true } - it { @project.private?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.private?).to be_truthy } end context 'should be internal when updated to internal' do @@ -29,8 +29,8 @@ describe Projects::UpdateService do update_project(@project, @user, @opts) end - it { @created_private.should be_true } - it { @project.internal?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.internal?).to be_truthy } end context 'should be public when updated to public' do @@ -41,15 +41,15 @@ describe Projects::UpdateService do update_project(@project, @user, @opts) end - it { @created_private.should be_true } - it { @project.public?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.public?).to be_truthy } end context 'respect configured visibility restrictions setting' do before(:each) do - @restrictions = double("restrictions") - @restrictions.stub(:restricted_visibility_levels) { [ "public" ] } - Settings.stub_chain(:gitlab).and_return(@restrictions) + allow_any_instance_of(ApplicationSetting).to( + receive(:restricted_visibility_levels).and_return([20]) + ) end context 'should be private when updated to private' do @@ -60,8 +60,8 @@ describe Projects::UpdateService do update_project(@project, @user, @opts) end - it { @created_private.should be_true } - it { @project.private?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.private?).to be_truthy } end context 'should be internal when updated to internal' do @@ -72,8 +72,8 @@ describe Projects::UpdateService do update_project(@project, @user, @opts) end - it { @created_private.should be_true } - it { @project.internal?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.internal?).to be_truthy } end context 'should be private when updated to public' do @@ -84,8 +84,8 @@ describe Projects::UpdateService do update_project(@project, @user, @opts) end - it { @created_private.should be_true } - it { @project.private?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.private?).to be_truthy } end context 'should be public when updated to public by admin' do @@ -96,8 +96,8 @@ describe Projects::UpdateService do update_project(@project, @admin, @opts) end - it { @created_private.should be_true } - it { @project.public?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.public?).to be_truthy } end end end diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb new file mode 100644 index 00000000000..e5c47015a03 --- /dev/null +++ b/spec/services/projects/upload_service_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Projects::UploadService do + describe 'File service' do + before do + @user = create :user + @project = create :project, creator_id: @user.id, namespace: @user.namespace + end + + context 'for valid gif file' do + before do + gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') + @link_to_file = upload_file(@project.repository, gif) + end + + it { expect(@link_to_file).to have_key('alt') } + it { expect(@link_to_file).to have_key('url') } + it { expect(@link_to_file).to have_key('is_image') } + it { expect(@link_to_file).to have_value('banana_sample') } + it { expect(@link_to_file['is_image']).to equal(true) } + it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match('banana_sample.gif') } + end + + context 'for valid png file' do + before do + png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', + 'image/png') + @link_to_file = upload_file(@project.repository, png) + end + + it { expect(@link_to_file).to have_key('alt') } + it { expect(@link_to_file).to have_key('url') } + it { expect(@link_to_file).to have_value('dk') } + it { expect(@link_to_file).to have_key('is_image') } + it { expect(@link_to_file['is_image']).to equal(true) } + it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match('dk.png') } + end + + context 'for valid jpg file' do + before do + jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') + @link_to_file = upload_file(@project.repository, jpg) + end + + it { expect(@link_to_file).to have_key('alt') } + it { expect(@link_to_file).to have_key('url') } + it { expect(@link_to_file).to have_key('is_image') } + it { expect(@link_to_file).to have_value('rails_sample') } + it { expect(@link_to_file['is_image']).to equal(true) } + it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match('rails_sample.jpg') } + end + + context 'for txt file' do + before do + txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') + @link_to_file = upload_file(@project.repository, txt) + end + + it { expect(@link_to_file).to have_key('alt') } + it { expect(@link_to_file).to have_key('url') } + it { expect(@link_to_file).to have_key('is_image') } + it { expect(@link_to_file).to have_value('doc_sample.txt') } + it { expect(@link_to_file['is_image']).to equal(false) } + it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match('doc_sample.txt') } + end + + context 'for too large a file' do + before do + txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') + allow(txt).to receive(:size) { 1000.megabytes.to_i } + @link_to_file = upload_file(@project.repository, txt) + end + + it { expect(@link_to_file).to eq(nil) } + end + end + + def upload_file(repository, file) + Projects::UploadService.new(repository, file).execute + end +end diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 3217c571e67..f57bfaea879 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -19,7 +19,7 @@ describe 'Search::GlobalService' do it 'should return public projects only' do context = Search::GlobalService.new(nil, search: "searchable") results = context.execute - results.objects('projects').should match_array [public_project] + expect(results.objects('projects')).to match_array [public_project] end end @@ -27,19 +27,19 @@ describe 'Search::GlobalService' do it 'should return public, internal and private projects' do context = Search::GlobalService.new(user, search: "searchable") results = context.execute - results.objects('projects').should match_array [public_project, found_project, internal_project] + expect(results.objects('projects')).to match_array [public_project, found_project, internal_project] end it 'should return only public & internal projects' do context = Search::GlobalService.new(internal_user, search: "searchable") results = context.execute - results.objects('projects').should match_array [internal_project, public_project] + expect(results.objects('projects')).to match_array [internal_project, public_project] end it 'namespace name should be searchable' do context = Search::GlobalService.new(user, search: found_project.namespace.path) results = context.execute - results.objects('projects').should match_array [found_project] + expect(results.objects('projects')).to match_array [found_project] end end end diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 573446d3a19..199ac996608 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -5,27 +5,58 @@ describe SystemHooksService do let (:project) { create :project } let (:project_member) { create :project_member } let (:key) { create(:key, user: user) } + let (:group) { create(:group) } + let (:group_member) { create(:group_member) } context 'event data' do - it { event_data(user, :create).should include(:event_name, :name, :created_at, :email, :user_id) } - it { event_data(user, :destroy).should include(:event_name, :name, :created_at, :email, :user_id) } - it { event_data(project, :create).should include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { event_data(project, :destroy).should include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { event_data(project_member, :create).should include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } - it { event_data(project_member, :destroy).should include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } - it { event_data(key, :create).should include(:username, :key, :id) } - it { event_data(key, :destroy).should include(:username, :key, :id) } + it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :email, :user_id) } + it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :email, :user_id) } + it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } + it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } + it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(key, :create)).to include(:username, :key, :id) } + it { expect(event_data(key, :destroy)).to include(:username, :key, :id) } + + it do + expect(event_data(group, :create)).to include( + :event_name, :name, :created_at, :path, :group_id, :owner_name, + :owner_email + ) + end + it do + expect(event_data(group, :destroy)).to include( + :event_name, :name, :created_at, :path, :group_id, :owner_name, + :owner_email + ) + end + it do + expect(event_data(group_member, :create)).to include( + :event_name, :created_at, :group_name, :group_path, :group_id, :user_id, + :user_name, :user_email, :group_access + ) + end + it do + expect(event_data(group_member, :destroy)).to include( + :event_name, :created_at, :group_name, :group_path, :group_id, :user_id, + :user_name, :user_email, :group_access + ) + end end context 'event names' do - it { event_name(user, :create).should eq "user_create" } - it { event_name(user, :destroy).should eq "user_destroy" } - it { event_name(project, :create).should eq "project_create" } - it { event_name(project, :destroy).should eq "project_destroy" } - it { event_name(project_member, :create).should eq "user_add_to_team" } - it { event_name(project_member, :destroy).should eq "user_remove_from_team" } - it { event_name(key, :create).should eq 'key_create' } - it { event_name(key, :destroy).should eq 'key_destroy' } + it { expect(event_name(user, :create)).to eq "user_create" } + it { expect(event_name(user, :destroy)).to eq "user_destroy" } + it { expect(event_name(project, :create)).to eq "project_create" } + it { expect(event_name(project, :destroy)).to eq "project_destroy" } + it { expect(event_name(project_member, :create)).to eq "user_add_to_team" } + it { expect(event_name(project_member, :destroy)).to eq "user_remove_from_team" } + it { expect(event_name(key, :create)).to eq 'key_create' } + it { expect(event_name(key, :destroy)).to eq 'key_destroy' } + it { expect(event_name(group, :create)).to eq 'group_create' } + it { expect(event_name(group, :destroy)).to eq 'group_destroy' } + it { expect(event_name(group_member, :create)).to eq 'user_add_to_group' } + it { expect(event_name(group_member, :destroy)).to eq 'user_remove_from_group' } end def event_data(*args) diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb new file mode 100644 index 00000000000..2658576640c --- /dev/null +++ b/spec/services/system_note_service_spec.rb @@ -0,0 +1,390 @@ +require 'spec_helper' + +describe SystemNoteService do + let(:project) { create(:project) } + let(:author) { create(:user) } + let(:noteable) { create(:issue, project: project) } + + shared_examples_for 'a system note' do + it 'is valid' do + expect(subject).to be_valid + end + + it 'sets the noteable model' do + expect(subject.noteable).to eq noteable + end + + it 'sets the project' do + expect(subject.project).to eq project + end + + it 'sets the author' do + expect(subject.author).to eq author + end + + it 'is a system note' do + expect(subject).to be_system + end + end + + describe '.add_commits' do + subject { described_class.add_commits(noteable, project, author, new_commits, old_commits, oldrev) } + + let(:noteable) { create(:merge_request, source_project: project) } + let(:new_commits) { noteable.commits } + let(:old_commits) { [] } + let(:oldrev) { nil } + + it_behaves_like 'a system note' + + describe 'note body' do + let(:note_lines) { subject.note.split("\n").reject(&:blank?) } + + context 'without existing commits' do + it 'adds a message header' do + expect(note_lines[0]).to eq "Added #{new_commits.size} commits:" + end + + it 'adds a message line for each commit' do + new_commits.each_with_index do |commit, i| + # Skip the header + expect(note_lines[i + 1]).to eq "* #{commit.short_id} - #{commit.title}" + end + end + end + + describe 'summary line for existing commits' do + let(:summary_line) { note_lines[1] } + + context 'with one existing commit' do + let(:old_commits) { [noteable.commits.last] } + + it 'includes the existing commit' do + expect(summary_line).to eq "* #{old_commits.first.short_id} - 1 commit from branch `feature`" + end + end + + context 'with multiple existing commits' do + let(:old_commits) { noteable.commits[3..-1] } + + context 'with oldrev' do + let(:oldrev) { noteable.commits[2].id } + + it 'includes a commit range' do + expect(summary_line).to start_with "* #{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id}" + end + + it 'includes a commit count' do + expect(summary_line).to end_with " - 2 commits from branch `feature`" + end + end + + context 'without oldrev' do + it 'includes a commit range' do + expect(summary_line).to start_with "* #{old_commits[0].short_id}..#{old_commits[-1].short_id}" + end + + it 'includes a commit count' do + expect(summary_line).to end_with " - 2 commits from branch `feature`" + end + end + + context 'on a fork' do + before do + expect(noteable).to receive(:for_fork?).and_return(true) + end + + it 'includes the project namespace' do + expect(summary_line).to end_with "`#{noteable.target_project_namespace}:feature`" + end + end + end + end + end + end + + describe '.change_assignee' do + subject { described_class.change_assignee(noteable, project, author, assignee) } + + let(:assignee) { create(:user) } + + it_behaves_like 'a system note' + + context 'when assignee added' do + it 'sets the note text' do + expect(subject.note).to eq "Reassigned to @#{assignee.username}" + end + end + + context 'when assignee removed' do + let(:assignee) { nil } + + it 'sets the note text' do + expect(subject.note).to eq 'Assignee removed' + end + end + end + + describe '.change_label' do + subject { described_class.change_label(noteable, project, author, added, removed) } + + let(:labels) { create_list(:label, 2) } + let(:added) { [] } + let(:removed) { [] } + + it_behaves_like 'a system note' + + context 'with added labels' do + let(:added) { labels } + let(:removed) { [] } + + it 'sets the note text' do + expect(subject.note).to eq "Added ~#{labels[0].id} ~#{labels[1].id} labels" + end + end + + context 'with removed labels' do + let(:added) { [] } + let(:removed) { labels } + + it 'sets the note text' do + expect(subject.note).to eq "Removed ~#{labels[0].id} ~#{labels[1].id} labels" + end + end + + context 'with added and removed labels' do + let(:added) { [labels[0]] } + let(:removed) { [labels[1]] } + + it 'sets the note text' do + expect(subject.note).to eq "Added ~#{labels[0].id} and removed ~#{labels[1].id} labels" + end + end + end + + describe '.change_milestone' do + subject { described_class.change_milestone(noteable, project, author, milestone) } + + let(:milestone) { create(:milestone, project: project) } + + it_behaves_like 'a system note' + + context 'when milestone added' do + it 'sets the note text' do + expect(subject.note).to eq "Milestone changed to #{milestone.title}" + end + end + + context 'when milestone removed' do + let(:milestone) { nil } + + it 'sets the note text' do + expect(subject.note).to eq 'Milestone removed' + end + end + end + + describe '.change_status' do + subject { described_class.change_status(noteable, project, author, status, source) } + + let(:status) { 'new_status' } + let(:source) { nil } + + it_behaves_like 'a system note' + + context 'with a source' do + let(:source) { double('commit', gfm_reference: 'commit 123456') } + + it 'sets the note text' do + expect(subject.note).to eq "Status changed to #{status} by commit 123456" + end + end + + context 'without a source' do + it 'sets the note text' do + expect(subject.note).to eq "Status changed to #{status}" + end + end + end + + describe '.change_title' do + subject { described_class.change_title(noteable, project, author, 'Old title') } + + context 'when noteable responds to `title`' do + it_behaves_like 'a system note' + + it 'sets the note text' do + expect(subject.note). + to eq "Title changed from **Old title** to **#{noteable.title}**" + end + end + + context 'when noteable does not respond to `title' do + let(:noteable) { double('noteable') } + + it 'returns nil' do + expect(subject).to be_nil + end + end + end + + describe '.change_branch' do + subject { described_class.change_branch(noteable, project, author, 'target', old_branch, new_branch) } + let(:old_branch) { 'old_branch'} + let(:new_branch) { 'new_branch'} + + it_behaves_like 'a system note' + + context 'when target branch name changed' do + it 'sets the note text' do + expect(subject.note).to eq "Target branch changed from `#{old_branch}` to `#{new_branch}`" + end + end + end + + describe '.cross_reference' do + subject { described_class.cross_reference(noteable, mentioner, author) } + + let(:mentioner) { create(:issue, project: project) } + + it_behaves_like 'a system note' + + context 'when cross-reference disallowed' do + before do + expect(described_class).to receive(:cross_reference_disallowed?).and_return(true) + end + + it 'returns nil' do + expect(subject).to be_nil + end + end + + context 'when cross-reference allowed' do + before do + expect(described_class).to receive(:cross_reference_disallowed?).and_return(false) + end + + describe 'note_body' do + context 'cross-project' do + let(:project2) { create(:project) } + let(:mentioner) { create(:issue, project: project2) } + + context 'from Commit' do + let(:mentioner) { project2.repository.commit } + + it 'references the mentioning commit' do + expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}" + end + end + + context 'from non-Commit' do + it 'references the mentioning object' do + expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}" + end + end + end + + context 'within the same project' do + context 'from Commit' do + let(:mentioner) { project.repository.commit } + + it 'references the mentioning commit' do + expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}" + end + end + + context 'from non-Commit' do + it 'references the mentioning object' do + expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}" + end + end + end + end + end + end + + describe '.cross_reference?' do + it 'is truthy when text begins with expected text' do + expect(described_class.cross_reference?('mentioned in something')).to be_truthy + end + + it 'is falsey when text does not begin with expected text' do + expect(described_class.cross_reference?('this is a note')).to be_falsey + end + end + + describe '.cross_reference_disallowed?' do + context 'when mentioner is not a MergeRequest' do + it 'is falsey' do + mentioner = noteable.dup + expect(described_class.cross_reference_disallowed?(noteable, mentioner)). + to be_falsey + end + end + + context 'when mentioner is a MergeRequest' do + let(:mentioner) { create(:merge_request, :simple, source_project: project) } + let(:noteable) { project.commit } + + it 'is truthy when noteable is in commits' do + expect(mentioner).to receive(:commits).and_return([noteable]) + expect(described_class.cross_reference_disallowed?(noteable, mentioner)). + to be_truthy + end + + it 'is falsey when noteable is not in commits' do + expect(mentioner).to receive(:commits).and_return([]) + expect(described_class.cross_reference_disallowed?(noteable, mentioner)). + to be_falsey + end + end + + context 'when notable is an ExternalIssue' do + let(:noteable) { ExternalIssue.new('EXT-1234', project) } + it 'is truthy' do + mentioner = noteable.dup + expect(described_class.cross_reference_disallowed?(noteable, mentioner)). + to be_truthy + end + end + end + + describe '.cross_reference_exists?' do + let(:commit0) { project.commit } + let(:commit1) { project.commit('HEAD~2') } + + context 'issue from commit' do + before do + # Mention issue (noteable) from commit0 + described_class.cross_reference(noteable, commit0, author) + end + + it 'is truthy when already mentioned' do + expect(described_class.cross_reference_exists?(noteable, commit0)). + to be_truthy + end + + it 'is falsey when not already mentioned' do + expect(described_class.cross_reference_exists?(noteable, commit1)). + to be_falsey + end + end + + context 'commit from commit' do + before do + # Mention commit1 from commit0 + described_class.cross_reference(commit0, commit1, author) + end + + it 'is truthy when already mentioned' do + expect(described_class.cross_reference_exists?(commit0, commit1)). + to be_truthy + end + + it 'is falsey when not already mentioned' do + expect(described_class.cross_reference_exists?(commit1, commit0)). + to be_falsey + end + end + end +end diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb index 76af5bf7b88..d2b505f55a2 100644 --- a/spec/services/test_hook_service_spec.rb +++ b/spec/services/test_hook_service_spec.rb @@ -8,7 +8,7 @@ describe TestHookService do describe :execute do it "should execute successfully" do stub_request(:post, hook.url).to_return(status: 200) - TestHookService.new.execute(hook, user).should be_true + expect(TestHookService.new.execute(hook, user)).to be_truthy end end end diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb new file mode 100644 index 00000000000..841ef9bfed1 --- /dev/null +++ b/spec/services/update_snippet_service_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe UpdateSnippetService do + before do + @user = create :user + @admin = create :user, admin: true + @opts = { + title: 'Test snippet', + file_name: 'snippet.rb', + content: 'puts "hello world"', + visibility_level: Gitlab::VisibilityLevel::PRIVATE + } + end + + context 'When public visibility is restricted' do + before do + allow_any_instance_of(ApplicationSetting).to( + receive(:restricted_visibility_levels).and_return( + [Gitlab::VisibilityLevel::PUBLIC] + ) + ) + + @snippet = create_snippet(@project, @user, @opts) + @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + + it 'non-admins should not be able to update to public visibility' do + old_visibility = @snippet.visibility_level + update_snippet(@project, @user, @snippet, @opts) + expect(@snippet.errors.messages).to have_key(:visibility_level) + expect(@snippet.errors.messages[:visibility_level].first).to( + match('Public visibility has been restricted') + ) + expect(@snippet.visibility_level).to eq(old_visibility) + end + + it 'admins should be able to update to pubic visibility' do + old_visibility = @snippet.visibility_level + update_snippet(@project, @admin, @snippet, @opts) + expect(@snippet.visibility_level).not_to eq(old_visibility) + expect(@snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + + def create_snippet(project, user, opts) + CreateSnippetService.new(project, user, opts).execute + end + + def update_snippet(project = nil, user, snippet, opts) + UpdateSnippetService.new(project, user, snippet, opts).execute + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 773de6628b1..666d56079d7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,30 +1,12 @@ -if ENV['SIMPLECOV'] - require 'simplecov' -end - -if ENV['COVERALLS'] - require 'coveralls' - Coveralls.wear_merged! -end - ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' -require 'capybara/rails' -require 'capybara/rspec' -require 'webmock/rspec' -require 'email_spec' +require 'shoulda/matchers' require 'sidekiq/testing/inline' -require 'capybara/poltergeist' - -Capybara.javascript_driver = :poltergeist -Capybara.default_wait_time = 10 # 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} - -WebMock.disable_net_connect!(allow_localhost: true) +Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } RSpec.configure do |config| config.use_transactional_fixtures = false @@ -37,8 +19,12 @@ RSpec.configure do |config| config.include Devise::TestHelpers, type: :controller config.include TestEnv + config.infer_spec_type_from_file_location! + config.raise_errors_for_deprecations! config.before(:suite) do TestEnv.init end end + +ActiveRecord::Migration.maintain_test_schema! diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index ec9a326a1ea..f63322776d4 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -29,6 +29,6 @@ module ApiHelpers end def json_response - JSON.parse(response.body) + @_json_response ||= JSON.parse(response.body) end end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb new file mode 100644 index 00000000000..fed1ab6ee33 --- /dev/null +++ b/spec/support/capybara.rb @@ -0,0 +1,21 @@ +require 'capybara/rails' +require 'capybara/rspec' +require 'capybara/poltergeist' + +# Give CI some extra time +timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 + +Capybara.javascript_driver = :poltergeist +Capybara.register_driver :poltergeist do |app| + Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout) +end + +Capybara.default_wait_time = timeout +Capybara.ignore_hidden_elements = true + +unless ENV['CI'] || ENV['CI_SERVER'] + require 'capybara-screenshot/rspec' + + # Keep only the screenshots generated from the last failing test suite + Capybara::Screenshot.prune_strategy = :keep_last_run +end diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb new file mode 100644 index 00000000000..9b5c3065eed --- /dev/null +++ b/spec/support/capybara_helpers.rb @@ -0,0 +1,34 @@ +module CapybaraHelpers + # Execute a block a certain number of times before considering it a failure + # + # The given block is called, and if it raises a `Capybara::ExpectationNotMet` + # error, we wait `interval` seconds and then try again, until `retries` is + # met. + # + # This allows for better handling of timing-sensitive expectations in a + # sketchy CI environment, for example. + # + # interval - Delay between retries in seconds (default: 0.5) + # retries - Number of times to execute before failing (default: 5) + def allowing_for_delay(interval: 0.5, retries: 5) + tries = 0 + + begin + sleep interval + + yield + rescue Capybara::ExpectationNotMet => ex + if tries <= retries + tries += 1 + sleep interval + retry + else + raise ex + end + end + end +end + +RSpec.configure do |config| + config.include CapybaraHelpers, type: :feature +end diff --git a/spec/support/coverage.rb b/spec/support/coverage.rb new file mode 100644 index 00000000000..a54bf03380c --- /dev/null +++ b/spec/support/coverage.rb @@ -0,0 +1,8 @@ +if ENV['SIMPLECOV'] + require 'simplecov' +end + +if ENV['COVERALLS'] + require 'coveralls' + Coveralls.wear_merged! +end diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index d2d532d9738..65d31433dab 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -1,21 +1,3 @@ -# RSpec.configure do |config| - -# config.around(:each) do |example| -# DatabaseCleaner.strategy = :transaction -# DatabaseCleaner.clean_with(:truncation) -# DatabaseCleaner.cleaning do -# example.run -# end -# end - -# config.around(:each, js: true) do |example| -# DatabaseCleaner.strategy = :truncation -# DatabaseCleaner.clean_with(:truncation) -# DatabaseCleaner.cleaning do -# example.run -# end -# end -# end RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.clean_with(:truncation) diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb new file mode 100644 index 00000000000..755964e9a3d --- /dev/null +++ b/spec/support/filter_spec_helper.rb @@ -0,0 +1,77 @@ +# Helper methods for Gitlab::Markdown filter specs +# +# Must be included into specs manually +module FilterSpecHelper + extend ActiveSupport::Concern + + # Perform `call` on the described class + # + # Automatically passes the current `project` value, if defined, to the context + # if none is provided. + # + # html - HTML String to pass to the filter's `call` method. + # contexts - Hash context for the filter. (default: {project: project}) + # + # Returns a Nokogiri::XML::DocumentFragment + def filter(html, contexts = {}) + if defined?(project) + contexts.reverse_merge!(project: project) + end + + described_class.call(html, contexts) + end + + # Run text through HTML::Pipeline with the current filter and return the + # result Hash + # + # body - String text to run through the pipeline + # contexts - Hash context for the filter. (default: {project: project}) + # + # Returns the Hash + def pipeline_result(body, contexts = {}) + contexts.reverse_merge!(project: project) + + pipeline = HTML::Pipeline.new([described_class], contexts) + pipeline.call(body) + end + + # Modify a String reference to make it invalid + # + # Commit SHAs get reversed, IDs get incremented by 1, all other Strings get + # their word characters reversed. + # + # reference - String reference to modify + # + # Returns a String + def invalidate_reference(reference) + if reference =~ /\A(.+)?.\d+\z/ + # Integer-based reference with optional project prefix + reference.gsub(/\d+\z/) { |i| i.to_i + 1 } + elsif reference =~ /\A(.+@)?(\h{6,40}\z)/ + # SHA-based reference with optional prefix + reference.gsub(/\h{6,40}\z/) { |v| v.reverse } + else + reference.gsub(/\w+\z/) { |v| v.reverse } + end + end + + # Stub CrossProjectReference#user_can_reference_project? to return true for + # the current test + def allow_cross_reference! + allow_any_instance_of(described_class). + to receive(:user_can_reference_project?).and_return(true) + end + + # Stub CrossProjectReference#user_can_reference_project? to return false for + # the current test + def disallow_cross_reference! + allow_any_instance_of(described_class). + to receive(:user_can_reference_project?).and_return(false) + end + + # Shortcut to Rails' auto-generated routes helpers, to avoid including the + # module + def urls + Rails.application.routes.url_helpers + end +end diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index 791d2a1fd64..ffe30a4246c 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -1,9 +1,25 @@ module LoginHelpers - # Internal: Create and log in as a user of the specified role + # Internal: Log in as a specific user or a new user of a specific role # - # role - User role (e.g., :admin, :user) - def login_as(role) - @user = create(role) + # user_or_role - User object, or a role to create (e.g., :admin, :user) + # + # Examples: + # + # # Create a user automatically + # login_as(:user) + # + # # Create an admin automatically + # login_as(:admin) + # + # # Provide an existing User record + # user = create(:user) + # login_as(user) + def login_as(user_or_role) + if user_or_role.kind_of?(User) + @user = user_or_role + else + @user = create(user_or_role) + end login_with(@user) end @@ -23,4 +39,9 @@ module LoginHelpers def logout find(:css, ".fa.fa-sign-out").click end + + # Logout without JavaScript driver + def logout_direct + page.driver.submit :delete, '/users/sign_out', {} + end end diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index 52b11bd6323..e5ebc6e7ec8 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -1,30 +1,43 @@ RSpec::Matchers.define :be_valid_commit do match do |actual| - actual != nil - actual.id == ValidCommit::ID - actual.message == ValidCommit::MESSAGE - actual.author_name == ValidCommit::AUTHOR_FULL_NAME + actual && + actual.id == ValidCommit::ID && + actual.message == ValidCommit::MESSAGE && + actual.author_name == ValidCommit::AUTHOR_FULL_NAME end end +def emulate_user(user) + user = case user + when :user then create(:user) + when :visitor then nil + when :admin then create(:admin) + else user + end + login_with(user) if user +end + RSpec::Matchers.define :be_allowed_for do |user| match do |url| - include UrlAccess - url_allowed?(user, url) + emulate_user(user) + visit url + status_code != 404 && current_path != new_user_session_path end end RSpec::Matchers.define :be_denied_for do |user| match do |url| - include UrlAccess - url_denied?(user, url) + emulate_user(user) + visit url + status_code == 404 || current_path == new_user_session_path end end -RSpec::Matchers.define :be_404_for do |user| +RSpec::Matchers.define :be_not_found_for do |user| match do |url| - include UrlAccess - url_404?(user, url) + emulate_user(user) + visit url + status_code == 404 end end @@ -33,44 +46,18 @@ RSpec::Matchers.define :include_module do |expected| described_class.included_modules.include?(expected) end - failure_message_for_should do - "expected #{described_class} to include the #{expected} module" + description do + "includes the #{expected} module" end -end -module UrlAccess - def url_allowed?(user, url) - emulate_user(user) - visit url - (status_code != 404 && current_path != new_user_session_path) - end - - def url_denied?(user, url) - emulate_user(user) - visit url - (status_code == 404 || current_path == new_user_session_path) - end - - def url_404?(user, url) - emulate_user(user) - visit url - status_code == 404 - end - - def emulate_user(user) - user = case user - when :user then create(:user) - when :visitor then nil - when :admin then create(:admin) - else user - end - login_with(user) if user + failure_message do + "expected #{described_class} to include the #{expected} module" end end # Extend shoulda-matchers module Shoulda::Matchers::ActiveModel - class EnsureLengthOfMatcher + class ValidateLengthOfMatcher # Shortcut for is_at_least and is_at_most def is_within(range) is_at_least(range.min) && is_at_most(range.max) diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index ebd74206699..a2a0b6905f9 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -1,24 +1,21 @@ # Specifications for behavior common to all Mentionable implementations. # Requires a shared context containing: -# - let(:subject) { "the mentionable implementation" } +# - subject { "the mentionable implementation" } # - let(:backref_text) { "the way that +subject+ should refer to itself in backreferences " } # - let(:set_mentionable_text) { lambda { |txt| "block that assigns txt to the subject's mentionable_text" } } def common_mentionable_setup - # Avoid name collisions with let(:project) or let(:author) in the surrounding scope. - let(:mproject) { create :project } - let(:mauthor) { subject.author } + let(:project) { create :project } + let(:author) { subject.author } - let(:mentioned_issue) { create :issue, project: mproject } - let(:other_issue) { create :issue, project: mproject } - let(:mentioned_mr) { create :merge_request, :simple, source_project: mproject } - let(:mentioned_commit) { double('commit', sha: '1234567890abcdef').as_null_object } + let(:mentioned_issue) { create(:issue, project: project) } + let(:mentioned_mr) { create(:merge_request, :simple, source_project: project) } + let(:mentioned_commit) { project.commit } - let(:ext_proj) { create :project, :public } - let(:ext_issue) { create :issue, project: ext_proj } - let(:other_ext_issue) { create :issue, project: ext_proj } - let(:ext_mr) { create :merge_request, :simple, source_project: ext_proj } - let(:ext_commit) { ext_proj.repository.commit } + let(:ext_proj) { create(:project, :public) } + let(:ext_issue) { create(:issue, project: ext_proj) } + let(:ext_mr) { create(:merge_request, :simple, source_project: ext_proj) } + let(:ext_commit) { ext_proj.commit } # Override to add known commits to the repository stub. let(:extra_commits) { [] } @@ -26,20 +23,35 @@ def common_mentionable_setup # A string that mentions each of the +mentioned_.*+ objects above. Mentionables should add a self-reference # to this string and place it in their +mentionable_text+. let(:ref_string) do - "mentions ##{mentioned_issue.iid} twice ##{mentioned_issue.iid}, " + - "!#{mentioned_mr.iid}, " + - "#{ext_proj.path_with_namespace}##{ext_issue.iid}, " + - "#{ext_proj.path_with_namespace}!#{ext_mr.iid}, " + - "#{ext_proj.path_with_namespace}@#{ext_commit.short_id}, " + - "#{mentioned_commit.sha[0..10]} and itself as #{backref_text}" + <<-MSG.strip_heredoc + These references are new: + Issue: #{mentioned_issue.to_reference} + Merge: #{mentioned_mr.to_reference} + Commit: #{mentioned_commit.to_reference} + + This reference is a repeat and should only be mentioned once: + Repeat: #{mentioned_issue.to_reference} + + These references are cross-referenced: + Issue: #{ext_issue.to_reference(project)} + Merge: #{ext_mr.to_reference(project)} + Commit: #{ext_commit.to_reference(project)} + + This is a self-reference and should not be mentioned at all: + Self: #{backref_text} + MSG end before do - # Wire the project's repository to return the mentioned commit, and +nil+ for any - # unrecognized commits. - commitmap = { '1234567890a' => mentioned_commit } + # Wire the project's repository to return the mentioned commit, and +nil+ + # for any unrecognized commits. + commitmap = { + mentioned_commit.id => mentioned_commit + } extra_commits.each { |c| commitmap[c.short_id] = c } - mproject.repository.stub(:commit) { |sha| commitmap[sha] } + + allow(project.repository).to receive(:commit) { |sha| commitmap[sha] } + set_mentionable_text.call(ref_string) end end @@ -48,19 +60,19 @@ shared_examples 'a mentionable' do common_mentionable_setup it 'generates a descriptive back-reference' do - subject.gfm_reference.should == backref_text + expect(subject.gfm_reference).to eq(backref_text) end it "extracts references from its reference property" do # De-duplicate and omit itself - refs = subject.references(mproject) - refs.should have(6).items - refs.should include(mentioned_issue) - refs.should include(mentioned_mr) - refs.should include(mentioned_commit) - refs.should include(ext_issue) - refs.should include(ext_mr) - refs.should include(ext_commit) + refs = subject.references(project) + expect(refs.size).to eq(6) + expect(refs).to include(mentioned_issue) + expect(refs).to include(mentioned_mr) + expect(refs).to include(mentioned_commit) + expect(refs).to include(ext_issue) + expect(refs).to include(ext_mr) + expect(refs).to include(ext_commit) end it 'creates cross-reference notes' do @@ -68,17 +80,18 @@ shared_examples 'a mentionable' do ext_issue, ext_mr, ext_commit] mentioned_objects.each do |referenced| - Note.should_receive(:create_cross_reference_note).with(referenced, subject.local_reference, mauthor, mproject) + expect(SystemNoteService).to receive(:cross_reference). + with(referenced, subject.local_reference, author) end - subject.create_cross_references!(mproject, mauthor) + subject.create_cross_references!(project, author) end it 'detects existing cross-references' do - Note.create_cross_reference_note(mentioned_issue, subject.local_reference, mauthor, mproject) + SystemNoteService.cross_reference(mentioned_issue, subject.local_reference, author) - subject.has_mentioned?(mentioned_issue).should be_true - subject.has_mentioned?(mentioned_mr).should be_false + expect(subject).to have_mentioned(mentioned_issue) + expect(subject).not_to have_mentioned(mentioned_mr) end end @@ -87,29 +100,49 @@ shared_examples 'an editable mentionable' do it_behaves_like 'a mentionable' + let(:new_issues) do + [create(:issue, project: project), create(:issue, project: ext_proj)] + end + it 'creates new cross-reference notes when the mentionable text is edited' do - new_text = "still mentions ##{mentioned_issue.iid}, " + - "#{mentioned_commit.sha[0..10]}, " + - "#{ext_issue.iid}, " + - "new refs: ##{other_issue.iid}, " + - "#{ext_proj.path_with_namespace}##{other_ext_issue.iid}" + subject.save + + new_text = <<-MSG.strip_heredoc + These references already existed: + + Issue: #{mentioned_issue.to_reference} + + Commit: #{mentioned_commit.to_reference} + --- + + This cross-project reference already existed: + + Issue: #{ext_issue.to_reference(project)} + + --- + + These two references are introduced in an edit: + + Issue: #{new_issues[0].to_reference} + + Cross: #{new_issues[1].to_reference(project)} + MSG + + # These three objects were already referenced, and should not receive new + # notes [mentioned_issue, mentioned_commit, ext_issue].each do |oldref| - Note.should_not_receive(:create_cross_reference_note).with(oldref, subject.local_reference, - mauthor, mproject) + expect(SystemNoteService).not_to receive(:cross_reference). + with(oldref, any_args) end - [other_issue, other_ext_issue].each do |newref| - Note.should_receive(:create_cross_reference_note).with( - newref, - subject.local_reference, - mauthor, - mproject - ) + # These two issues are new and should receive reference notes + new_issues.each do |newref| + expect(SystemNoteService).to receive(:cross_reference). + with(newref, subject.local_reference, author) end - subject.save set_mentionable_text.call(new_text) - subject.notice_added_references(mproject, mauthor) + subject.notice_added_references(project, author) end end diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb index 4c4775da692..aadf791bf3f 100644 --- a/spec/support/repo_helpers.rb +++ b/spec/support/repo_helpers.rb @@ -43,6 +43,25 @@ eos ) end + def another_sample_commit + OpenStruct.new( + id: "e56497bb5f03a90a51293fc6d516788730953899", + parent_id: '4cd80ccab63c82b4bad16faa5193fbd2aa06df40', + author_full_name: "Sytse Sijbrandij", + author_email: "sytse@gitlab.com", + files_changed_count: 1, + message: <<eos +Add directory structure for tree_helper spec + +This directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module + +See [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774) + +See merge request !2 +eos + ) + end + def sample_big_commit OpenStruct.new( id: "913c66a37b4a45b9769037c55c2d238bd0942d2e", diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb index c7cf109a7bb..691f84f39d4 100644 --- a/spec/support/select2_helper.rb +++ b/spec/support/select2_helper.rb @@ -17,9 +17,9 @@ module Select2Helper selector = options[:from] if options[:multiple] - execute_script("$('#{selector}').select2('val', ['#{value}']);") + execute_script("$('#{selector}').select2('val', ['#{value}'], true);") else - execute_script("$('#{selector}').select2('val', '#{value}');") + execute_script("$('#{selector}').select2('val', '#{value}', true);") end end end diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb index 42252675683..927c72c7409 100644 --- a/spec/support/taskable_shared_examples.rb +++ b/spec/support/taskable_shared_examples.rb @@ -1,42 +1,32 @@ # Specs for task state functionality for issues and merge requests. # # Requires a context containing: -# let(:subject) { Issue or MergeRequest } +# subject { Issue or MergeRequest } shared_examples 'a Taskable' do before do - subject.description = <<EOT.gsub(/ {6}/, '') + subject.description = <<-EOT.strip_heredoc * [ ] Task 1 * [x] Task 2 * [x] Task 3 * [ ] Task 4 * [ ] Task 5 -EOT - end - - it 'updates the Nth task correctly' do - subject.update_nth_task(1, true) - expect(subject.description).to match(/\[x\] Task 1/) - - subject.update_nth_task(2, true) - expect(subject.description).to match('\[x\] Task 2') - - subject.update_nth_task(3, false) - expect(subject.description).to match('\[ \] Task 3') - - subject.update_nth_task(4, false) - expect(subject.description).to match('\[ \] Task 4') + EOT end it 'returns the correct task status' do expect(subject.task_status).to match('5 tasks') - expect(subject.task_status).to match('2 done') - expect(subject.task_status).to match('3 unfinished') + expect(subject.task_status).to match('2 completed') + expect(subject.task_status).to match('3 remaining') end - it 'knows if it has tasks' do - expect(subject.tasks?).to be_true + describe '#tasks?' do + it 'returns true when object has tasks' do + expect(subject.tasks?).to eq true + end - subject.description = 'Now I have no tasks' - expect(subject.tasks?).to be_false + it 'returns false when object has no tasks' do + subject.description = 'Now I have no tasks' + expect(subject.tasks?).to eq false + end end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index e6db410fb1c..8bdd6b43cdd 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -5,6 +5,7 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { + 'flatten-dir' => 'e56497b', 'feature' => '0b4bc9a', 'feature_conflict' => 'bb5206f', 'fix' => '12d65c8', @@ -13,26 +14,19 @@ module TestEnv 'master' => '5937ac0' } + FORKED_BRANCH_SHA = BRANCH_SHA.merge({ + 'add-submodule-version-bump' => '3f547c08' + }) + # Test environment # # See gitlab.yml.example test section for paths # def init(opts = {}) - RSpec::Mocks::setup(self) - # Disable mailer for spinach tests disable_mailer if opts[:mailer] == false - # Clean /tmp/tests - tmp_test_path = Rails.root.join('tmp', 'tests') - - if File.directory?(tmp_test_path) - Dir.entries(tmp_test_path).each do |entry| - unless ['.', '..', 'gitlab-shell', factory_repo_name].include?(entry) - FileUtils.rm_r(File.join(tmp_test_path, entry)) - end - end - end + clean_test_path FileUtils.mkdir_p(repos_path) @@ -41,29 +35,61 @@ module TestEnv # Create repository for FactoryGirl.create(:project) setup_factory_repo + + # Create repository for FactoryGirl.create(:forked_project_with_submodules) + setup_forked_repo end def disable_mailer - NotificationService.any_instance.stub(mailer: double.as_null_object) + allow_any_instance_of(NotificationService).to receive(:mailer). + and_return(double.as_null_object) end def enable_mailer - NotificationService.any_instance.unstub(:mailer) + allow_any_instance_of(NotificationService).to receive(:mailer). + and_call_original + end + + # Clean /tmp/tests + # + # Keeps gitlab-shell and gitlab-test + def clean_test_path + tmp_test_path = Rails.root.join('tmp', 'tests', '**') + + Dir[tmp_test_path].each do |entry| + unless File.basename(entry) =~ /\Agitlab-(shell|test|test-fork)\z/ + FileUtils.rm_rf(entry) + end + end end def setup_gitlab_shell - `rake gitlab:shell:install` + unless File.directory?(Rails.root.join(*%w(tmp tests gitlab-shell))) + `rake gitlab:shell:install` + end end def setup_factory_repo - clone_url = "https://gitlab.com/gitlab-org/#{factory_repo_name}.git" + setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name, + BRANCH_SHA) + end + + # This repo has a submodule commit that is not present in the main test + # repository. + def setup_forked_repo + setup_repo(forked_repo_path, forked_repo_path_bare, forked_repo_name, + FORKED_BRANCH_SHA) + end - unless File.directory?(factory_repo_path) - system(*%W(git clone #{clone_url} #{factory_repo_path})) + def setup_repo(repo_path, repo_path_bare, repo_name, branch_sha) + clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git" + + unless File.directory?(repo_path) + system(*%W(git clone -q #{clone_url} #{repo_path})) end - Dir.chdir(factory_repo_path) do - BRANCH_SHA.each do |branch, sha| + Dir.chdir(repo_path) do + branch_sha.each do |branch, sha| # Try to reset without fetching to avoid using the network. reset = %W(git update-ref refs/heads/#{branch} #{sha}) unless system(*reset) @@ -80,7 +106,7 @@ module TestEnv end # We must copy bare repositories because we will push to them. - system(*%W(git clone --bare #{factory_repo_path} #{factory_repo_path_bare})) + system(git_env, *%W(git clone -q --bare #{repo_path} #{repo_path_bare})) end def copy_repo(project) @@ -95,6 +121,14 @@ module TestEnv Gitlab.config.gitlab_shell.repos_path end + def copy_forked_repo_with_submodules(project) + base_repo_path = File.expand_path(forked_repo_path_bare) + target_repo_path = File.expand_path(repos_path + "/#{project.namespace.path}/#{project.path}.git") + FileUtils.mkdir_p(target_repo_path) + FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) + FileUtils.chmod_R 0755, target_repo_path + end + private def factory_repo_path @@ -102,10 +136,29 @@ module TestEnv end def factory_repo_path_bare - factory_repo_path.to_s + '_bare' + "#{factory_repo_path}_bare" end def factory_repo_name 'gitlab-test' end + + def forked_repo_path + @forked_repo_path ||= Rails.root.join('tmp', 'tests', forked_repo_name) + end + + def forked_repo_path_bare + "#{forked_repo_path}_bare" + end + + def forked_repo_name + 'gitlab-test-fork' + end + + + # Prevent developer git configurations from being persisted to test + # repositories + def git_env + { 'GIT_TEMPLATE_DIR' => '' } + end end diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb new file mode 100644 index 00000000000..af2906b7568 --- /dev/null +++ b/spec/support/webmock.rb @@ -0,0 +1,4 @@ +require 'webmock' +require 'webmock/rspec' + +WebMock.disable_net_connect!(allow_localhost: true) diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 71a45eb2fa6..2f90b67aef1 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -10,43 +10,147 @@ describe 'gitlab:app namespace rake task' do Rake::Task.define_task :environment end + def run_rake_task(task_name) + Rake::Task[task_name].reenable + Rake.application.invoke_task task_name + end + describe 'backup_restore' do before do # avoid writing task output to spec progress - $stdout.stub :write - end - - let :run_rake_task do - Rake::Task["gitlab:backup:restore"].reenable - Rake.application.invoke_task "gitlab:backup:restore" + allow($stdout).to receive :write end context 'gitlab version' do before do - Dir.stub glob: [] - Dir.stub :chdir - File.stub exists?: true - Kernel.stub system: true - FileUtils.stub cp_r: true - FileUtils.stub mv: true - Rake::Task["gitlab:shell:setup"].stub invoke: true + allow(Dir).to receive(:glob).and_return([]) + allow(Dir).to receive(:chdir) + allow(File).to receive(:exists?).and_return(true) + allow(Kernel).to receive(:system).and_return(true) + allow(FileUtils).to receive(:cp_r).and_return(true) + allow(FileUtils).to receive(:mv).and_return(true) + allow(Rake::Task["gitlab:shell:setup"]). + to receive(:invoke).and_return(true) end let(:gitlab_version) { Gitlab::VERSION } it 'should fail on mismatch' do - YAML.stub load_file: {gitlab_version: "not #{gitlab_version}" } - expect { run_rake_task }.to raise_error SystemExit + allow(YAML).to receive(:load_file). + and_return({gitlab_version: "not #{gitlab_version}" }) + + expect { run_rake_task('gitlab:backup:restore') }. + to raise_error(SystemExit) end it 'should invoke restoration on mach' do - YAML.stub load_file: {gitlab_version: gitlab_version} - Rake::Task["gitlab:backup:db:restore"].should_receive :invoke - Rake::Task["gitlab:backup:repo:restore"].should_receive :invoke - Rake::Task["gitlab:shell:setup"].should_receive :invoke - expect { run_rake_task }.to_not raise_error + allow(YAML).to receive(:load_file). + and_return({gitlab_version: gitlab_version}) + expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke) + expect(Rake::Task["gitlab:backup:repo: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 end end # backup_restore task + + describe 'backup_create' do + def tars_glob + Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')) + end + + before :all do + # Record the existing backup tars so we don't touch them + existing_tars = tars_glob + + # Redirect STDOUT and run the rake task + orig_stdout = $stdout + $stdout = StringIO.new + run_rake_task('gitlab:backup:create') + $stdout = orig_stdout + + @backup_tar = (tars_glob - existing_tars).first + end + + after :all do + FileUtils.rm(@backup_tar) + end + + it 'should set correct permissions on the tar file' do + expect(File.exist?(@backup_tar)).to be_truthy + expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600') + end + + it 'should set correct permissions on the tar contents' do + tar_contents, exit_status = Gitlab::Popen.popen( + %W{tar -tvf #{@backup_tar} db uploads repositories} + ) + expect(exit_status).to eq(0) + expect(tar_contents).to match('db/') + expect(tar_contents).to match('uploads/') + expect(tar_contents).to match('repositories/') + expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories)\/$/) + end + + it 'should delete temp directories' do + temp_dirs = Dir.glob( + File.join(Gitlab.config.backup.path, '{db,repositories,uploads}') + ) + + expect(temp_dirs).to be_empty + end + end # backup_create task + + describe "Skipping items" do + def tars_glob + Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')) + end + + before :all do + @origin_cd = Dir.pwd + + Rake::Task["gitlab:backup:db:create"].reenable + Rake::Task["gitlab:backup:repo:create"].reenable + Rake::Task["gitlab:backup:uploads:create"].reenable + + # Record the existing backup tars so we don't touch them + existing_tars = tars_glob + + # Redirect STDOUT and run the rake task + orig_stdout = $stdout + $stdout = StringIO.new + ENV["SKIP"] = "repositories" + run_rake_task('gitlab:backup:create') + $stdout = orig_stdout + + @backup_tar = (tars_glob - existing_tars).first + end + + after :all do + FileUtils.rm(@backup_tar) + Dir.chdir @origin_cd + end + + it "does not contain skipped item" do + tar_contents, exit_status = Gitlab::Popen.popen( + %W{tar -tvf #{@backup_tar} db uploads repositories} + ) + + expect(tar_contents).to match('db/') + expect(tar_contents).to match('uploads/') + expect(tar_contents).not_to match('repositories/') + end + + it 'does not invoke repositories restore' do + allow(Rake::Task["gitlab:shell:setup"]). + to receive(:invoke).and_return(true) + allow($stdout).to receive :write + + expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke + expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke + expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke + expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error + end + end end # gitlab:app namespace diff --git a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb new file mode 100644 index 00000000000..37feb5e6faf --- /dev/null +++ b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb @@ -0,0 +1,27 @@ +require 'spec_helper' +require 'rake' + +describe 'gitlab:mail_google_schema_whitelisting rake task' do + before :all do + Rake.application.rake_require "tasks/gitlab/task_helpers" + Rake.application.rake_require "tasks/gitlab/mail_google_schema_whitelisting" + # empty task as env is already loaded + Rake::Task.define_task :environment + end + + describe 'call' do + before do + # avoid writing task output to spec progress + allow($stdout).to receive :write + end + + let :run_rake_task do + Rake::Task["gitlab:mail_google_schema_whitelisting"].reenable + Rake.application.invoke_task "gitlab:mail_google_schema_whitelisting" + end + + it 'should run the task without errors' do + expect { run_rake_task }.not_to raise_error + end + end +end diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb new file mode 100644 index 00000000000..58f45ff8610 --- /dev/null +++ b/spec/teaspoon_env.rb @@ -0,0 +1,178 @@ +Teaspoon.configure do |config| + # Determines where the Teaspoon routes will be mounted. Changing this to "/jasmine" would allow you to browse to + # `http://localhost:3000/jasmine` to run your tests. + config.mount_at = "/teaspoon" + + # Specifies the root where Teaspoon will look for files. If you're testing an engine using a dummy application it can + # be useful to set this to your engines root (e.g. `Teaspoon::Engine.root`). + # Note: Defaults to `Rails.root` if nil. + config.root = nil + + # Paths that will be appended to the Rails assets paths + # Note: Relative to `config.root`. + config.asset_paths = ["spec/javascripts", "spec/javascripts/stylesheets"] + + # Fixtures are rendered through a controller, which allows using HAML, RABL/JBuilder, etc. Files in these paths will + # be rendered as fixtures. + config.fixture_paths = ["spec/javascripts/fixtures"] + + # SUITES + # + # You can modify the default suite configuration and create new suites here. Suites are isolated from one another. + # + # When defining a suite you can provide a name and a block. If the name is left blank, :default is assumed. You can + # omit various directives and the ones defined in the default suite will be used. + # + # To run a specific suite + # - in the browser: http://localhost/teaspoon/[suite_name] + # - with the rake task: rake teaspoon suite=[suite_name] + # - with the cli: teaspoon --suite=[suite_name] + config.suite do |suite| + # Specify the framework you would like to use. This allows you to select versions, and will do some basic setup for + # you -- which you can override with the directives below. This should be specified first, as it can override other + # directives. + # Note: If no version is specified, the latest is assumed. + # + # Versions: 1.3.1, 2.0.3, 2.1.3, 2.2.0 + suite.use_framework :jasmine, "2.2.0" + + # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These + # files need to be within an asset path. You can add asset paths using the `config.asset_paths`. + 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 = [] + + # 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"] + + # This suites spec helper, which can require additional support files. This file is loaded before any of your test + # files are loaded. + suite.helper = "spec_helper" + + # Partial to be rendered in the head tag of the runner. You can use the provided ones or define your own by creating + # a `_boot.html.erb` in your fixtures path, and adjust the config to `"/boot"` for instance. + # + # Available: boot, boot_require_js + suite.boot_partial = "boot" + + # Partial to be rendered in the body tag of the runner. You can define your own to create a custom body structure. + suite.body_partial = "body" + + # 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{} + + # 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, + # Teaspoon expands all assets to provide more valuable stack traces that reference individual source files. + #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| + # suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}" + #end + + # CONSOLE RUNNER SPECIFIC + # + # These configuration directives are applicable only when running via the rake task or command line interface. These + # directives can be overridden using the command line interface arguments or with ENV variables when using the rake + # task. + # + # Command Line Interface: + # teaspoon --driver=phantomjs --server-port=31337 --fail-fast=true --format=junit --suite=my_suite /spec/file_spec.js + # + # Rake: + # teaspoon DRIVER=phantomjs SERVER_PORT=31337 FAIL_FAST=true FORMATTERS=junit suite=my_suite + + # Specify which headless driver to use. Supports PhantomJS and Selenium Webdriver. + # + # Available: :phantomjs, :selenium, :capybara_webkit + # 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 + + # 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 + + # 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 + + # Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used. + #config.server = nil + + # Specify a port to run on a specific port, otherwise Teaspoon will use a random available port. + #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 + + # 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 + + # 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] + + # Specify if you want color output from the formatters. + #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 + + # COVERAGE REPORTS / THRESHOLD ASSERTIONS + # + # Coverage reports requires Istanbul (https://github.com/gotwarlost/istanbul) to add instrumentation to your code and + # display coverage statistics. + # + # Coverage configurations are similar to suites. You can define several, and use different ones under different + # conditions. + # + # To run with a specific coverage configuration + # - with the rake task: rake teaspoon USE_COVERAGE=[coverage_name] + # - with the cli: teaspoon --coverage=[coverage_name] + + # 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 + + # You can have multiple coverage configs by passing a name to config.coverage. + # e.g. config.coverage :ci do |coverage| + # The default coverage config name is :default. + config.coverage do |coverage| + # 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"] + + # 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" + + # 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.}] + + # 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 + end +end diff --git a/spec/workers/fork_registration_worker_spec.rb b/spec/workers/fork_registration_worker_spec.rb new file mode 100644 index 00000000000..cc6f574b29c --- /dev/null +++ b/spec/workers/fork_registration_worker_spec.rb @@ -0,0 +1,10 @@ + +require 'spec_helper' + +describe ForkRegistrationWorker do + context "as a resque worker" do + it "reponds to #perform" do + expect(ForkRegistrationWorker.new).to respond_to(:perform) + end + end +end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 4273fd1019a..46eae9ab081 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -1,9 +1,13 @@ require 'spec_helper' describe PostReceive do + let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" } + let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") } + let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } + context "as a resque worker" do it "reponds to #perform" do - PostReceive.new.should respond_to(:perform) + expect(PostReceive.new).to respond_to(:perform) end end @@ -13,33 +17,29 @@ describe PostReceive do let(:key_id) { key.shell_id } it "fetches the correct project" do - Project.should_receive(:find_with_namespace).with(project.path_with_namespace).and_return(project) - PostReceive.new.perform(pwd(project), key_id, changes) + expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project) + PostReceive.new.perform(pwd(project), key_id, base64_changes) end it "does not run if the author is not in the project" do - Key.stub(:find_by).with(hash_including(id: anything())) { nil } + allow(Key).to receive(:find_by).with(hash_including(id: anything())) { nil } - project.should_not_receive(:execute_hooks) + expect(project).not_to receive(:execute_hooks) - PostReceive.new.perform(pwd(project), key_id, changes).should be_false + expect(PostReceive.new.perform(pwd(project), key_id, base64_changes)).to be_falsey end it "asks the project to trigger all hooks" do - Project.stub(find_with_namespace: project) - project.should_receive(:execute_hooks) - project.should_receive(:execute_services) - project.should_receive(:update_merge_requests) + allow(Project).to receive(:find_with_namespace).and_return(project) + expect(project).to receive(:execute_hooks).twice + expect(project).to receive(:execute_services).twice + expect(project).to receive(:update_merge_requests) - PostReceive.new.perform(pwd(project), key_id, changes) + PostReceive.new.perform(pwd(project), key_id, base64_changes) end end def pwd(project) File.join(Gitlab.config.gitlab_shell.repos_path, project.path_with_namespace) end - - def changes - 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master' - end end diff --git a/spec/workers/repository_archive_worker_spec.rb b/spec/workers/repository_archive_worker_spec.rb new file mode 100644 index 00000000000..c2362058cfd --- /dev/null +++ b/spec/workers/repository_archive_worker_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe RepositoryArchiveWorker do + let(:project) { create(:project) } + subject { RepositoryArchiveWorker.new } + + before do + allow(Project).to receive(:find).and_return(project) + end + + describe "#perform" do + it "cleans old archives" do + expect(project.repository).to receive(:clean_old_archives) + + subject.perform(project.id, "master", "zip") + end + + context "when the repository doesn't have an archive file path" do + before do + allow(project.repository).to receive(:archive_file_path).and_return(nil) + end + + it "doesn't archive the repo" do + expect(project.repository).not_to receive(:archive_repo) + + subject.perform(project.id, "master", "zip") + end + end + + context "when the repository has an archive file path" do + let(:file_path) { "/archive.zip" } + let(:pid_file_path) { "/archive.zip.pid" } + + before do + allow(project.repository).to receive(:archive_file_path).and_return(file_path) + allow(project.repository).to receive(:archive_pid_file_path).and_return(pid_file_path) + end + + context "when the archive file already exists" do + before do + allow(File).to receive(:exist?).with(file_path).and_return(true) + end + + it "doesn't archive the repo" do + expect(project.repository).not_to receive(:archive_repo) + + subject.perform(project.id, "master", "zip") + end + end + + context "when the archive file doesn't exist yet" do + before do + allow(File).to receive(:exist?).with(file_path).and_return(false) + allow(File).to receive(:exist?).with(pid_file_path).and_return(true) + end + + context "when the archive pid file doesn't exist yet" do + before do + allow(File).to receive(:exist?).with(pid_file_path).and_return(false) + end + + it "archives the repo" do + expect(project.repository).to receive(:archive_repo) + + subject.perform(project.id, "master", "zip") + end + end + + context "when the archive pid file already exists" do + it "doesn't archive the repo" do + expect(project.repository).not_to receive(:archive_repo) + + subject.perform(project.id, "master", "zip") + end + end + end + end + end +end + diff --git a/vendor/assets/javascripts/chart-lib.min.js b/vendor/assets/javascripts/chart-lib.min.js index 626e6c3cdb9..3a0a2c87345 100755..100644 --- a/vendor/assets/javascripts/chart-lib.min.js +++ b/vendor/assets/javascripts/chart-lib.min.js @@ -1,11 +1,11 @@ /*! * Chart.js * http://chartjs.org/ - * Version: 1.0.1-beta.4 + * Version: 1.0.2 * - * Copyright 2014 Nick Downie + * Copyright 2015 Nick Downie * Released under the MIT license * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md */ -(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;this.width=t.canvas.width,this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n<t.length;n++)i.apply(e,[t[n],n].concat(s))}else for(var o in t)i.apply(e,[t[o],o].concat(s))},o=s.clone=function(t){var i={};return n(t,function(e,s){t.hasOwnProperty(s)&&(i[s]=e)}),i},a=s.extend=function(t){return n(Array.prototype.slice.call(arguments,1),function(i){n(i,function(e,s){i.hasOwnProperty(s)&&(t[s]=e)})}),t},h=s.merge=function(){var t=Array.prototype.slice.call(arguments,0);return t.unshift({}),a.apply(null,t)},l=s.indexOf=function(t,i){if(Array.prototype.indexOf)return t.indexOf(i);for(var e=0;e<t.length;e++)if(t[e]===i)return e;return-1},r=(s.where=function(t,i){var e=[];return s.each(t,function(t){i(t)&&e.push(t)}),e},s.findNextWhere=function(t,i,e){e||(e=-1);for(var s=e+1;s<t.length;s++){var n=t[s];if(i(n))return n}},s.findPreviousWhere=function(t,i,e){e||(e=t.length);for(var s=e-1;s>=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof t.define&&t.define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),x=s.radians=function(t){return t*(Math.PI/180)},S=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),x=Math.round(f/v);(x>a||a>2*x)&&!h;)if(x>a)v*=2,x=Math.round(f/v),x%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,x=Math.round(f/v)}else v/=2,x=Math.round(f/v);return h&&(x=o,v=f/x),{steps:x,stepValue:v,min:p,max:p+x*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),b=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),-(s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)))},easeOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),s*Math.pow(2,-10*t)*Math.sin(2*(1*t-i)*Math.PI/e)+1)},easeInOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:2==(t/=.5)?1:(e||(e=.3*1.5),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),1>t?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-b.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*b.easeInBounce(2*t):.5*b.easeOutBounce(2*t-1)+.5}}),w=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=(s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),s.animationLoop=function(t,i,e,s,n,o){var a=0,h=b[e]||b.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=w(l):n.apply(o)};w(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),L=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},k=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},P(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){L(t.chart.canvas,e,i)})}),F=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},R=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),T=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},M=s.fontString=function(t,i,e){return i+" "+t+"px "+e},W=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},z=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return T(this.chart),this},stop:function(){return s.cancelAnimFrame.call(t,this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=F(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:R(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),k(this,this.events),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)<Math.pow(e,2)},draw:function(){if(this.display){var t=this.ctx;t.beginPath(),t.arc(this.x,this.y,this.radius,0,2*Math.PI),t.closePath(),t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.fillStyle=this.fillColor,t.fill(),t.stroke()}}}),e.Arc=e.Element.extend({inRange:function(t,i){var e=s.getAngleFromPoint(this,{x:t,y:i}),n=e.angle>=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;switch(t.fillStyle=this.fillColor,this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}z(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=M(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=W(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){z(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?W(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),t<this.yLabelWidth&&this.calculateXLabelRotation()},calculateXLabelRotation:function(){this.ctx.font=this.font;var t,i,e=this.ctx.measureText(this.xLabels[0]).width,s=this.ctx.measureText(this.xLabels[this.xLabels.length-1]).width;if(this.xScalePaddingRight=s/2+3,this.xScalePaddingLeft=e/2>this.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=W(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(x(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(x(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/(this.valuesCount-(this.offsetGridLines?0:1)),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a);t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath(),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+S(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+S(this.lineWidth),o=this.xLabelRotation>0;t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath(),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*x(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=M(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;i<this.valuesCount;i++)t=this.getPointPosition(i,d),e=this.ctx.measureText(C(this.templateString,{value:this.labels[i]})).width+5,0===i||i===this.valuesCount/2?(s=e/2,t.x+s>p&&(p=t.x+s,n=i),t.x-s<g&&(g=t.x-s,a=i)):i<this.valuesCount/2?t.x+e>p&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e<g&&(g=t.x-e,a=i);l=g,r=Math.ceil(p-this.width),o=this.getIndexAngle(n),h=this.getIndexAngle(a),c=r/Math.sin(o+Math.PI/2),u=l/Math.sin(h+Math.PI/2),c=f(c)?c:0,u=f(u)?u:0,this.drawingArea=d-(u+c)/2,this.setCenterPoint(u,c)},setCenterPoint:function(t,i){var e=this.width-i-this.drawingArea,s=t+this.drawingArea;this.xCenter=(s+e)/2,this.yCenter=this.height/2},getIndexAngle:function(t){var i=2*Math.PI/this.valuesCount;return t*i-Math.PI/2},getPointPosition:function(t,i){var e=this.getIndexAngle(t);return{x:Math.cos(e)*i+this.xCenter,y:Math.sin(e)*i+this.yCenter}},draw:function(){if(this.display){var t=this.ctx;if(n(this.yLabels,function(i,e){if(e>0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a<this.valuesCount;a++)s=this.getPointPosition(a,this.calculateCenterOffset(this.min+e*this.stepValue)),0===a?t.moveTo(s.x,s.y):t.lineTo(s.x,s.y);t.closePath(),t.stroke()}if(this.showLabels){if(t.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.showLabelBackdrop){var h=t.measureText(i).width;t.fillStyle=this.backdropColor,t.fillRect(this.xCenter-h/2-this.backdropPaddingX,o-this.fontSize/2-this.backdropPaddingY,h+2*this.backdropPaddingX,this.fontSize+2*this.backdropPaddingY)}t.textAlign="center",t.textBaseline="middle",t.fillStyle=this.fontColor,t.fillText(i,this.xCenter,o)}}},this),!this.lineArc){t.lineWidth=this.angleLineWidth,t.strokeStyle=this.angleLineColor;for(var i=this.valuesCount-1;i>=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=M(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<this.datasets.length;a++)for(i=0;i<this.datasets[a].bars.length;i++)if(this.datasets[a].bars[i].inRange(n.x,n.y))return e.each(this.datasets,o),s;return s},buildScale:function(t){var i=this,s=function(){var t=[];return i.eachBars(function(i){t.push(i.value)}),t},n={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(s(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.barShowStroke?this.options.barStrokeWidth:0,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(n,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new this.ScaleClass(n)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].bars.push(new this.BarClass({value:t,label:i,x:this.scale.calculateBarX(this.datasets.length,e,this.scale.valuesCount+1),y:this.scale.endPoint,width:this.scale.calculateBarWidth(this.datasets.length),base:this.scale.endPoint,strokeColor:this.datasets[e].strokeColor,fillColor:this.datasets[e].fillColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.bars.shift()},this),this.update()},reflow:function(){e.extend(this.BarClass.prototype,{y:this.scale.endPoint,base:this.scale.endPoint});var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();this.chart.ctx;this.scale.draw(i),e.each(this.datasets,function(t,s){e.each(t.bars,function(t,e){t.hasValue()&&(t.base=this.scale.endPoint,t.transition({x:this.scale.calculateBarX(this.datasets.length,s,e),y:this.scale.calculateY(t.value),width:this.scale.calculateBarWidth(this.datasets.length)},i).draw())},this)},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'}; -i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(t/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle)},this)}}),i.types.Doughnut.extend({name:"Pie",defaults:e.merge(s,{percentageInnerCutout:0})})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0,bezierCurveTension:.4,pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)<Math.pow(this.radius+this.hitDetectionRadius,2)}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this),this.buildScale(t.labels),this.eachPoints(function(t,i){e.extend(t,{x:this.scale.calculateX(i),y:this.scale.endPoint}),t.save()},this)},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachPoints(function(t){t.save()}),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.datasets,function(t){e.each(t.points,function(t){t.inRange(s.x,s.y)&&i.push(t)})},this),i},buildScale:function(t){var s=this,n=function(){var t=[];return s.eachPoints(function(i){t.push(i.value)}),t},o={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(n(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.pointDotRadius+this.options.pointDotStrokeWidth,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(o,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new i.Scale(o)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:this.scale.calculateX(this.scale.valuesCount+1),y:this.scale.endPoint,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.points.shift()},this),this.update()},reflow:function(){var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();var s=this.chart.ctx,n=function(t){return null!==t.value},o=function(t,i,s){return e.findNextWhere(i,n,s)||t},a=function(t,i,s){return e.findPreviousWhere(i,n,s)||t};this.scale.draw(i),e.each(this.datasets,function(t){var h=e.where(t.points,n);e.each(t.points,function(t,e){t.hasValue()&&t.transition({y:this.scale.calculateY(t.value),x:this.scale.calculateX(e)},i)},this),this.options.bezierCurve&&e.each(h,function(t,i){var s=i>0&&i<h.length-1?this.options.bezierCurveTension:0;t.controlPoints=e.splineCurve(a(t,h,i),t,o(t,h,i),s),t.controlPoints.outer.y>this.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.y<this.scale.startPoint&&(t.controlPoints.outer.y=this.scale.startPoint),t.controlPoints.inner.y>this.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y<this.scale.startPoint&&(t.controlPoints.inner.y=this.scale.startPoint)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(h,function(t,i){if(0===i)s.moveTo(t.x,t.y);else if(this.options.bezierCurve){var e=a(t,h,i);s.bezierCurveTo(e.controlPoints.outer.x,e.controlPoints.outer.y,t.controlPoints.inner.x,t.controlPoints.inner.y,t.x,t.y)}else s.lineTo(t.x,t.y)},this),s.stroke(),this.options.datasetFill&&h.length>0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle),t.draw()},this),this.scale.draw()}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers;i.Type.extend({name:"Radar",defaults:{scaleShowLine:!0,angleShowLineOut:!0,scaleShowLabels:!1,scaleBeginAtZero:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:10,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this);
\ No newline at end of file +(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;var i=function(t,i){return t["offset"+i]?t["offset"+i]:document.defaultView.getComputedStyle(t).getPropertyValue(i)},e=this.width=i(t.canvas,"Width"),n=this.height=i(t.canvas,"Height");t.canvas.width=e,t.canvas.height=n;var e=this.width=t.canvas.width,n=this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n<t.length;n++)i.apply(e,[t[n],n].concat(s))}else for(var o in t)i.apply(e,[t[o],o].concat(s))},o=s.clone=function(t){var i={};return n(t,function(e,s){t.hasOwnProperty(s)&&(i[s]=e)}),i},a=s.extend=function(t){return n(Array.prototype.slice.call(arguments,1),function(i){n(i,function(e,s){i.hasOwnProperty(s)&&(t[s]=e)})}),t},h=s.merge=function(){var t=Array.prototype.slice.call(arguments,0);return t.unshift({}),a.apply(null,t)},l=s.indexOf=function(t,i){if(Array.prototype.indexOf)return t.indexOf(i);for(var e=0;e<t.length;e++)if(t[e]===i)return e;return-1},r=(s.where=function(t,i){var e=[];return s.each(t,function(t){i(t)&&e.push(t)}),e},s.findNextWhere=function(t,i,e){e||(e=-1);for(var s=e+1;s<t.length;s++){var n=t[s];if(i(n))return n}},s.findPreviousWhere=function(t,i,e){e||(e=t.length);for(var s=e-1;s>=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),-(s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)))},easeOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),s*Math.pow(2,-10*t)*Math.sin(2*(1*t-i)*Math.PI/e)+1)},easeInOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:2==(t/=.5)?1:(e||(e=.3*1.5),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),1>t?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)<Math.pow(e,2)},draw:function(){if(this.display){var t=this.ctx;t.beginPath(),t.arc(this.x,this.y,this.radius,0,2*Math.PI),t.closePath(),t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.fillStyle=this.fillColor,t.fill(),t.stroke()}}}),e.Arc=e.Element.extend({inRange:function(t,i){var e=s.getAngleFromPoint(this,{x:t,y:i}),n=e.angle>=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),t<this.yLabelWidth&&this.calculateXLabelRotation()},calculateXLabelRotation:function(){this.ctx.font=this.font;var t,i,e=this.ctx.measureText(this.xLabels[0]).width,s=this.ctx.measureText(this.xLabels[this.xLabels.length-1]).width;if(this.xScalePaddingRight=s/2+3,this.xScalePaddingLeft=e/2>this.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;i<this.valuesCount;i++)t=this.getPointPosition(i,d),e=this.ctx.measureText(C(this.templateString,{value:this.labels[i]})).width+5,0===i||i===this.valuesCount/2?(s=e/2,t.x+s>p&&(p=t.x+s,n=i),t.x-s<g&&(g=t.x-s,a=i)):i<this.valuesCount/2?t.x+e>p&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e<g&&(g=t.x-e,a=i);l=g,r=Math.ceil(p-this.width),o=this.getIndexAngle(n),h=this.getIndexAngle(a),c=r/Math.sin(o+Math.PI/2),u=l/Math.sin(h+Math.PI/2),c=f(c)?c:0,u=f(u)?u:0,this.drawingArea=d-(u+c)/2,this.setCenterPoint(u,c)},setCenterPoint:function(t,i){var e=this.width-i-this.drawingArea,s=t+this.drawingArea;this.xCenter=(s+e)/2,this.yCenter=this.height/2},getIndexAngle:function(t){var i=2*Math.PI/this.valuesCount;return t*i-Math.PI/2},getPointPosition:function(t,i){var e=this.getIndexAngle(t);return{x:Math.cos(e)*i+this.xCenter,y:Math.sin(e)*i+this.yCenter}},draw:function(){if(this.display){var t=this.ctx;if(n(this.yLabels,function(i,e){if(e>0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a<this.valuesCount;a++)s=this.getPointPosition(a,this.calculateCenterOffset(this.min+e*this.stepValue)),0===a?t.moveTo(s.x,s.y):t.lineTo(s.x,s.y);t.closePath(),t.stroke()}if(this.showLabels){if(t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.showLabelBackdrop){var h=t.measureText(i).width;t.fillStyle=this.backdropColor,t.fillRect(this.xCenter-h/2-this.backdropPaddingX,o-this.fontSize/2-this.backdropPaddingY,h+2*this.backdropPaddingX,this.fontSize+2*this.backdropPaddingY)}t.textAlign="center",t.textBaseline="middle",t.fillStyle=this.fontColor,t.fillText(i,this.xCenter,o)}}},this),!this.lineArc){t.lineWidth=this.angleLineWidth,t.strokeStyle=this.angleLineColor;for(var i=this.valuesCount-1;i>=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<this.datasets.length;a++)for(i=0;i<this.datasets[a].bars.length;i++)if(this.datasets[a].bars[i].inRange(n.x,n.y))return e.each(this.datasets,o),s;return s},buildScale:function(t){var i=this,s=function(){var t=[];return i.eachBars(function(i){t.push(i.value)}),t},n={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(s(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,showHorizontalLines:this.options.scaleShowHorizontalLines,showVerticalLines:this.options.scaleShowVerticalLines,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.barShowStroke?this.options.barStrokeWidth:0,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(n,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new this.ScaleClass(n)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].bars.push(new this.BarClass({value:t,label:i,x:this.scale.calculateBarX(this.datasets.length,e,this.scale.valuesCount+1),y:this.scale.endPoint,width:this.scale.calculateBarWidth(this.datasets.length),base:this.scale.endPoint,strokeColor:this.datasets[e].strokeColor,fillColor:this.datasets[e].fillColor})) +},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.bars.shift()},this),this.update()},reflow:function(){e.extend(this.BarClass.prototype,{y:this.scale.endPoint,base:this.scale.endPoint});var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();this.chart.ctx;this.scale.draw(i),e.each(this.datasets,function(t,s){e.each(t.bars,function(t,e){t.hasValue()&&(t.base=this.scale.endPoint,t.transition({x:this.scale.calculateBarX(this.datasets.length,s,e),y:this.scale.calculateY(t.value),width:this.scale.calculateBarWidth(this.datasets.length)},i).draw())},this)},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle)},this)}}),i.types.Doughnut.extend({name:"Pie",defaults:e.merge(s,{percentageInnerCutout:0})})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,bezierCurve:!0,bezierCurveTension:.4,pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)<Math.pow(this.radius+this.hitDetectionRadius,2)}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this),this.buildScale(t.labels),this.eachPoints(function(t,i){e.extend(t,{x:this.scale.calculateX(i),y:this.scale.endPoint}),t.save()},this)},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachPoints(function(t){t.save()}),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.datasets,function(t){e.each(t.points,function(t){t.inRange(s.x,s.y)&&i.push(t)})},this),i},buildScale:function(t){var s=this,n=function(){var t=[];return s.eachPoints(function(i){t.push(i.value)}),t},o={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(n(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,showHorizontalLines:this.options.scaleShowHorizontalLines,showVerticalLines:this.options.scaleShowVerticalLines,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.pointDotRadius+this.options.pointDotStrokeWidth,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(o,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new i.Scale(o)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:this.scale.calculateX(this.scale.valuesCount+1),y:this.scale.endPoint,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.points.shift()},this),this.update()},reflow:function(){var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();var s=this.chart.ctx,n=function(t){return null!==t.value},o=function(t,i,s){return e.findNextWhere(i,n,s)||t},a=function(t,i,s){return e.findPreviousWhere(i,n,s)||t};this.scale.draw(i),e.each(this.datasets,function(t){var h=e.where(t.points,n);e.each(t.points,function(t,e){t.hasValue()&&t.transition({y:this.scale.calculateY(t.value),x:this.scale.calculateX(e)},i)},this),this.options.bezierCurve&&e.each(h,function(t,i){var s=i>0&&i<h.length-1?this.options.bezierCurveTension:0;t.controlPoints=e.splineCurve(a(t,h,i),t,o(t,h,i),s),t.controlPoints.outer.y>this.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.y<this.scale.startPoint&&(t.controlPoints.outer.y=this.scale.startPoint),t.controlPoints.inner.y>this.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y<this.scale.startPoint&&(t.controlPoints.inner.y=this.scale.startPoint)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(h,function(t,i){if(0===i)s.moveTo(t.x,t.y);else if(this.options.bezierCurve){var e=a(t,h,i);s.bezierCurveTo(e.controlPoints.outer.x,e.controlPoints.outer.y,t.controlPoints.inner.x,t.controlPoints.inner.y,t.x,t.y)}else s.lineTo(t.x,t.y)},this),s.stroke(),this.options.datasetFill&&h.length>0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle),t.draw()},this),this.scale.draw()}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers;i.Type.extend({name:"Radar",defaults:{scaleShowLine:!0,angleShowLineOut:!0,scaleShowLabels:!1,scaleBeginAtZero:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:10,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this);
\ No newline at end of file diff --git a/vendor/assets/javascripts/highlight.pack.js b/vendor/assets/javascripts/highlight.pack.js deleted file mode 100644 index c09eac02df9..00000000000 --- a/vendor/assets/javascripts/highlight.pack.js +++ /dev/null @@ -1 +0,0 @@ -var hljs=new function(){function j(v){return v.replace(/&/gm,"&").replace(/</gm,"<").replace(/>/gm,">")}function t(v){return v.nodeName.toLowerCase()}function h(w,x){var v=w&&w.exec(x);return v&&v.index==0}function r(w){var v=(w.className+" "+(w.parentNode?w.parentNode.className:"")).split(/\s+/);v=v.map(function(x){return x.replace(/^lang(uage)?-/,"")});return v.filter(function(x){return i(x)||/no(-?)highlight/.test(x)})[0]}function o(x,y){var v={};for(var w in x){v[w]=x[w]}if(y){for(var w in y){v[w]=y[w]}}return v}function u(x){var v=[];(function w(y,z){for(var A=y.firstChild;A;A=A.nextSibling){if(A.nodeType==3){z+=A.nodeValue.length}else{if(A.nodeType==1){v.push({event:"start",offset:z,node:A});z=w(A,z);if(!t(A).match(/br|hr|img|input/)){v.push({event:"stop",offset:z,node:A})}}}}return z})(x,0);return v}function q(w,y,C){var x=0;var F="";var z=[];function B(){if(!w.length||!y.length){return w.length?w:y}if(w[0].offset!=y[0].offset){return(w[0].offset<y[0].offset)?w:y}return y[0].event=="start"?w:y}function A(H){function G(I){return" "+I.nodeName+'="'+j(I.value)+'"'}F+="<"+t(H)+Array.prototype.map.call(H.attributes,G).join("")+">"}function E(G){F+="</"+t(G)+">"}function v(G){(G.event=="start"?A:E)(G.node)}while(w.length||y.length){var D=B();F+=j(C.substr(x,D[0].offset-x));x=D[0].offset;if(D==w){z.reverse().forEach(E);do{v(D.splice(0,1)[0]);D=B()}while(D==w&&D.length&&D[0].offset==x);z.reverse().forEach(A)}else{if(D[0].event=="start"){z.push(D[0].node)}else{z.pop()}v(D.splice(0,1)[0])}}return F+j(C.substr(x))}function m(y){function v(z){return(z&&z.source)||z}function w(A,z){return RegExp(v(A),"m"+(y.cI?"i":"")+(z?"g":""))}function x(D,C){if(D.compiled){return}D.compiled=true;D.k=D.k||D.bK;if(D.k){var z={};var E=function(G,F){if(y.cI){F=F.toLowerCase()}F.split(" ").forEach(function(H){var I=H.split("|");z[I[0]]=[G,I[1]?Number(I[1]):1]})};if(typeof D.k=="string"){E("keyword",D.k)}else{Object.keys(D.k).forEach(function(F){E(F,D.k[F])})}D.k=z}D.lR=w(D.l||/\b[A-Za-z0-9_]+\b/,true);if(C){if(D.bK){D.b="\\b("+D.bK.split(" ").join("|")+")\\b"}if(!D.b){D.b=/\B|\b/}D.bR=w(D.b);if(!D.e&&!D.eW){D.e=/\B|\b/}if(D.e){D.eR=w(D.e)}D.tE=v(D.e)||"";if(D.eW&&C.tE){D.tE+=(D.e?"|":"")+C.tE}}if(D.i){D.iR=w(D.i)}if(D.r===undefined){D.r=1}if(!D.c){D.c=[]}var B=[];D.c.forEach(function(F){if(F.v){F.v.forEach(function(G){B.push(o(F,G))})}else{B.push(F=="self"?D:F)}});D.c=B;D.c.forEach(function(F){x(F,D)});if(D.starts){x(D.starts,C)}var A=D.c.map(function(F){return F.bK?"\\.?("+F.b+")\\.?":F.b}).concat([D.tE,D.i]).map(v).filter(Boolean);D.t=A.length?w(A.join("|"),true):{exec:function(F){return null}}}x(y)}function c(T,L,J,R){function v(V,W){for(var U=0;U<W.c.length;U++){if(h(W.c[U].bR,V)){return W.c[U]}}}function z(V,U){if(h(V.eR,U)){return V}if(V.eW){return z(V.parent,U)}}function A(U,V){return !J&&h(V.iR,U)}function E(W,U){var V=M.cI?U[0].toLowerCase():U[0];return W.k.hasOwnProperty(V)&&W.k[V]}function w(aa,Y,X,W){var U=W?"":b.classPrefix,V='<span class="'+U,Z=X?"":"</span>";V+=aa+'">';return V+Y+Z}function N(){if(!I.k){return j(C)}var U="";var X=0;I.lR.lastIndex=0;var V=I.lR.exec(C);while(V){U+=j(C.substr(X,V.index-X));var W=E(I,V);if(W){H+=W[1];U+=w(W[0],j(V[0]))}else{U+=j(V[0])}X=I.lR.lastIndex;V=I.lR.exec(C)}return U+j(C.substr(X))}function F(){if(I.sL&&!f[I.sL]){return j(C)}var U=I.sL?c(I.sL,C,true,S):e(C);if(I.r>0){H+=U.r}if(I.subLanguageMode=="continuous"){S=U.top}return w(U.language,U.value,false,true)}function Q(){return I.sL!==undefined?F():N()}function P(W,V){var U=W.cN?w(W.cN,"",true):"";if(W.rB){D+=U;C=""}else{if(W.eB){D+=j(V)+U;C=""}else{D+=U;C=V}}I=Object.create(W,{parent:{value:I}})}function G(U,Y){C+=U;if(Y===undefined){D+=Q();return 0}var W=v(Y,I);if(W){D+=Q();P(W,Y);return W.rB?0:Y.length}var X=z(I,Y);if(X){var V=I;if(!(V.rE||V.eE)){C+=Y}D+=Q();do{if(I.cN){D+="</span>"}H+=I.r;I=I.parent}while(I!=X.parent);if(V.eE){D+=j(Y)}C="";if(X.starts){P(X.starts,"")}return V.rE?0:Y.length}if(A(Y,I)){throw new Error('Illegal lexeme "'+Y+'" for mode "'+(I.cN||"<unnamed>")+'"')}C+=Y;return Y.length||1}var M=i(T);if(!M){throw new Error('Unknown language: "'+T+'"')}m(M);var I=R||M;var S;var D="";for(var K=I;K!=M;K=K.parent){if(K.cN){D=w(K.cN,"",true)+D}}var C="";var H=0;try{var B,y,x=0;while(true){I.t.lastIndex=x;B=I.t.exec(L);if(!B){break}y=G(L.substr(x,B.index-x),B[0]);x=B.index+y}G(L.substr(x));for(var K=I;K.parent;K=K.parent){if(K.cN){D+="</span>"}}return{r:H,value:D,language:T,top:I}}catch(O){if(O.message.indexOf("Illegal")!=-1){return{r:0,value:j(L)}}else{throw O}}}function e(y,x){x=x||b.languages||Object.keys(f);var v={r:0,value:j(y)};var w=v;x.forEach(function(z){if(!i(z)){return}var A=c(z,y,false);A.language=z;if(A.r>w.r){w=A}if(A.r>v.r){w=v;v=A}});if(w.language){v.second_best=w}return v}function g(v){if(b.tabReplace){v=v.replace(/^((<[^>]+>|\t)+)/gm,function(w,z,y,x){return z.replace(/\t/g,b.tabReplace)})}if(b.useBR){v=v.replace(/\n/g,"<br>")}return v}function p(A){var B=r(A);if(/no(-?)highlight/.test(B)){return}var y;if(b.useBR){y=document.createElementNS("http://www.w3.org/1999/xhtml","div");y.innerHTML=A.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")}else{y=A}var z=y.textContent;var v=B?c(B,z,true):e(z);var x=u(y);if(x.length){var w=document.createElementNS("http://www.w3.org/1999/xhtml","div");w.innerHTML=v.value;v.value=q(x,u(w),z)}v.value=g(v.value);A.innerHTML=v.value;A.className+=" hljs "+(!B&&v.language||"");A.result={language:v.language,re:v.r};if(v.second_best){A.second_best={language:v.second_best.language,re:v.second_best.r}}}var b={classPrefix:"hljs-",tabReplace:null,useBR:false,languages:undefined};function s(v){b=o(b,v)}function l(){if(l.called){return}l.called=true;var v=document.querySelectorAll("pre code");Array.prototype.forEach.call(v,p)}function a(){addEventListener("DOMContentLoaded",l,false);addEventListener("load",l,false)}var f={};var n={};function d(v,x){var w=f[v]=x(this);if(w.aliases){w.aliases.forEach(function(y){n[y]=v})}}function k(){return Object.keys(f)}function i(v){return f[v]||f[n[v]]}this.highlight=c;this.highlightAuto=e;this.fixMarkup=g;this.highlightBlock=p;this.configure=s;this.initHighlighting=l;this.initHighlightingOnLoad=a;this.registerLanguage=d;this.listLanguages=k;this.getLanguage=i;this.inherit=o;this.IR="[a-zA-Z][a-zA-Z0-9_]*";this.UIR="[a-zA-Z_][a-zA-Z0-9_]*";this.NR="\\b\\d+(\\.\\d+)?";this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";this.BNR="\\b(0b[01]+)";this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\[\\s\\S]",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE]};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE]};this.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such)\b/};this.CLCM={cN:"comment",b:"//",e:"$",c:[this.PWM]};this.CBCM={cN:"comment",b:"/\\*",e:"\\*/",c:[this.PWM]};this.HCM={cN:"comment",b:"#",e:"$",c:[this.PWM]};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.CSSNM={cN:"number",b:this.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0};this.RM={cN:"regexp",b:/\//,e:/\/[gim]*/,i:/\n/,c:[this.BE,{b:/\[/,e:/\]/,r:0,c:[this.BE]}]};this.TM={cN:"title",b:this.IR,r:0};this.UTM={cN:"title",b:this.UIR,r:0}}();hljs.registerLanguage("1c",function(b){var f="[a-zA-Zа-яА-Я][a-zA-Z0-9_а-яА-Я]*";var c="возврат дата для если и или иначе иначеесли исключение конецесли конецпопытки конецпроцедуры конецфункции конеццикла константа не перейти перем перечисление по пока попытка прервать продолжить процедура строка тогда фс функция цикл число экспорт";var e="ansitooem oemtoansi ввестивидсубконто ввестидату ввестизначение ввестиперечисление ввестипериод ввестиплансчетов ввестистроку ввестичисло вопрос восстановитьзначение врег выбранныйплансчетов вызватьисключение датагод датамесяц датачисло добавитьмесяц завершитьработусистемы заголовоксистемы записьжурналарегистрации запуститьприложение зафиксироватьтранзакцию значениевстроку значениевстрокувнутр значениевфайл значениеизстроки значениеизстрокивнутр значениеизфайла имякомпьютера имяпользователя каталогвременныхфайлов каталогиб каталогпользователя каталогпрограммы кодсимв командасистемы конгода конецпериодаби конецрассчитанногопериодаби конецстандартногоинтервала конквартала конмесяца коннедели лев лог лог10 макс максимальноеколичествосубконто мин монопольныйрежим названиеинтерфейса названиенабораправ назначитьвид назначитьсчет найти найтипомеченныенаудаление найтиссылки началопериодаби началостандартногоинтервала начатьтранзакцию начгода начквартала начмесяца начнедели номерднягода номерднянедели номернеделигода нрег обработкаожидания окр описаниеошибки основнойжурналрасчетов основнойплансчетов основнойязык открытьформу открытьформумодально отменитьтранзакцию очиститьокносообщений периодстр полноеимяпользователя получитьвремята получитьдатута получитьдокументта получитьзначенияотбора получитьпозициюта получитьпустоезначение получитьта прав праводоступа предупреждение префиксавтонумерации пустаястрока пустоезначение рабочаядаттьпустоезначение рабочаядата разделительстраниц разделительстрок разм разобратьпозициюдокумента рассчитатьрегистрына рассчитатьрегистрыпо сигнал симв символтабуляции создатьобъект сокрл сокрлп сокрп сообщить состояние сохранитьзначение сред статусвозврата стрдлина стрзаменить стрколичествострок стрполучитьстроку стрчисловхождений сформироватьпозициюдокумента счетпокоду текущаядата текущеевремя типзначения типзначениястр удалитьобъекты установитьтана установитьтапо фиксшаблон формат цел шаблон";var a={cN:"dquote",b:'""'};var d={cN:"string",b:'"',e:'"|$',c:[a]};var g={cN:"string",b:"\\|",e:'"|$',c:[a]};return{cI:true,l:f,k:{keyword:c,built_in:e},c:[b.CLCM,b.NM,d,g,{cN:"function",b:"(процедура|функция)",e:"$",l:f,k:"процедура функция",c:[b.inherit(b.TM,{b:f}),{cN:"tail",eW:true,c:[{cN:"params",b:"\\(",e:"\\)",l:f,k:"знач",c:[d,g]},{cN:"export",b:"экспорт",eW:true,l:f,k:"экспорт",c:[b.CLCM]}]},b.CLCM]},{cN:"preprocessor",b:"#",e:"$"},{cN:"date",b:"'\\d{2}\\.\\d{2}\\.(\\d{2}|\\d{4})'"}]}});hljs.registerLanguage("actionscript",function(a){var c="[a-zA-Z_$][a-zA-Z0-9_$]*";var b="([*]|[a-zA-Z_$][a-zA-Z0-9_$]*)";var d={cN:"rest_arg",b:"[.]{3}",e:c,r:10};return{aliases:["as"],k:{keyword:"as break case catch class const continue default delete do dynamic each else extends final finally for function get if implements import in include instanceof interface internal is namespace native new override package private protected public return set static super switch this throw try typeof use var void while with",literal:"true false null undefined"},c:[a.ASM,a.QSM,a.CLCM,a.CBCM,a.CNM,{cN:"package",bK:"package",e:"{",c:[a.TM]},{cN:"class",bK:"class interface",e:"{",eE:true,c:[{bK:"extends implements"},a.TM]},{cN:"preprocessor",bK:"import include",e:";"},{cN:"function",bK:"function",e:"[{;]",eE:true,i:"\\S",c:[a.TM,{cN:"params",b:"\\(",e:"\\)",c:[a.ASM,a.QSM,a.CLCM,a.CBCM,d]},{cN:"type",b:":",e:b,r:10}]}]}});hljs.registerLanguage("apache",function(a){var b={cN:"number",b:"[\\$%]\\d+"};return{aliases:["apacheconf"],cI:true,c:[a.HCM,{cN:"tag",b:"</?",e:">"},{cN:"keyword",b:/\w+/,r:0,k:{common:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{e:/$/,r:0,k:{literal:"on off all"},c:[{cN:"sqbracket",b:"\\s\\[",e:"\\]$"},{cN:"cbracket",b:"[\\$%]\\{",e:"\\}",c:["self",b]},b,a.QSM]}}],i:/\S/}});hljs.registerLanguage("applescript",function(a){var b=a.inherit(a.QSM,{i:""});var d={cN:"params",b:"\\(",e:"\\)",c:["self",a.CNM,b]};var c=[{cN:"comment",b:"--",e:"$"},{cN:"comment",b:"\\(\\*",e:"\\*\\)",c:["self",{b:"--",e:"$"}]},a.HCM];return{aliases:["osascript"],k:{keyword:"about above after against and around as at back before beginning behind below beneath beside between but by considering contain contains continue copy div does eighth else end equal equals error every exit fifth first for fourth from front get given global if ignoring in into is it its last local me middle mod my ninth not of on onto or over prop property put ref reference repeat returning script second set seventh since sixth some tell tenth that the|0 then third through thru timeout times to transaction try until where while whose with without",constant:"AppleScript false linefeed return pi quote result space tab true",type:"alias application boolean class constant date file integer list number real record string text",command:"activate beep count delay launch log offset read round run say summarize write",property:"character characters contents day frontmost id item length month name paragraph paragraphs rest reverse running time version weekday word words year"},c:[b,a.CNM,{cN:"type",b:"\\bPOSIX file\\b"},{cN:"command",b:"\\b(clipboard info|the clipboard|info for|list (disks|folder)|mount volume|path to|(close|open for) access|(get|set) eof|current date|do shell script|get volume settings|random number|set volume|system attribute|system info|time to GMT|(load|run|store) script|scripting components|ASCII (character|number)|localized string|choose (application|color|file|file name|folder|from list|remote application|URL)|display (alert|dialog))\\b|^\\s*return\\b"},{cN:"constant",b:"\\b(text item delimiters|current application|missing value)\\b"},{cN:"keyword",b:"\\b(apart from|aside from|instead of|out of|greater than|isn't|(doesn't|does not) (equal|come before|come after|contain)|(greater|less) than( or equal)?|(starts?|ends|begins?) with|contained by|comes (before|after)|a (ref|reference))\\b"},{cN:"property",b:"\\b(POSIX path|(date|time) string|quoted form)\\b"},{cN:"function_start",bK:"on",i:"[${=;\\n]",c:[a.UTM,d]}].concat(c),i:"//"}});hljs.registerLanguage("xml",function(a){var c="[A-Za-z0-9\\._:-]+";var d={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php",subLanguageMode:"continuous"};var b={eW:true,i:/</,r:0,c:[d,{cN:"attribute",b:c,r:0},{b:"=",r:0,c:[{cN:"value",v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s\/>]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xsl","plist"],cI:true,c:[{cN:"doctype",b:"<!DOCTYPE",e:">",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"<!--",e:"-->",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"<style(?=\\s|>|$)",e:">",k:{title:"style"},c:[b],starts:{e:"</style>",rE:true,sL:"css"}},{cN:"tag",b:"<script(?=\\s|>|$)",e:">",k:{title:"script"},c:[b],starts:{e:"<\/script>",rE:true,sL:"javascript"}},{b:"<%",e:"%>",sL:"vbscript"},d,{cN:"pi",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"</?",e:"/?>",c:[{cN:"title",b:/[^ \/><\n\t]+/,r:0},b]}]}});hljs.registerLanguage("asciidoc",function(a){return{c:[{cN:"comment",b:"^/{4,}\\n",e:"\\n/{4,}$",r:10},{cN:"comment",b:"^//",e:"$",r:0},{cN:"title",b:"^\\.\\w.*$"},{b:"^[=\\*]{4,}\\n",e:"\\n^[=\\*]{4,}$",r:10},{cN:"header",b:"^(={1,5}) .+?( \\1)?$",r:10},{cN:"header",b:"^[^\\[\\]\\n]+?\\n[=\\-~\\^\\+]{2,}$",r:10},{cN:"attribute",b:"^:.+?:",e:"\\s",eE:true,r:10},{cN:"attribute",b:"^\\[.+?\\]$",r:0},{cN:"blockquote",b:"^_{4,}\\n",e:"\\n_{4,}$",r:10},{cN:"code",b:"^[\\-\\.]{4,}\\n",e:"\\n[\\-\\.]{4,}$",r:10},{b:"^\\+{4,}\\n",e:"\\n\\+{4,}$",c:[{b:"<",e:">",sL:"xml",r:0}],r:10},{cN:"bullet",b:"^(\\*+|\\-+|\\.+|[^\\n]+?::)\\s+"},{cN:"label",b:"^(NOTE|TIP|IMPORTANT|WARNING|CAUTION):\\s+",r:10},{cN:"strong",b:"\\B\\*(?![\\*\\s])",e:"(\\n{2}|\\*)",c:[{b:"\\\\*\\w",r:0}]},{cN:"emphasis",b:"\\B'(?!['\\s])",e:"(\\n{2}|')",c:[{b:"\\\\'\\w",r:0}],r:0},{cN:"emphasis",b:"_(?![_\\s])",e:"(\\n{2}|_)",r:0},{cN:"smartquote",b:"``.+?''",r:10},{cN:"smartquote",b:"`.+?'",r:10},{cN:"code",b:"(`.+?`|\\+.+?\\+)",r:0},{cN:"code",b:"^[ \\t]",e:"$",r:0},{cN:"horizontal_rule",b:"^'{3,}[ \\t]*$",r:10},{b:"(link:)?(http|https|ftp|file|irc|image:?):\\S+\\[.*?\\]",rB:true,c:[{b:"(link|image:?):",r:0},{cN:"link_url",b:"\\w",e:"[^\\[]+",r:0},{cN:"link_label",b:"\\[",e:"\\]",eB:true,eE:true,r:0}],r:10}]}});hljs.registerLanguage("autohotkey",function(b){var d={cN:"escape",b:"`[\\s\\S]"};var c={cN:"comment",b:";",e:"$",r:0};var a=[{cN:"built_in",b:"A_[a-zA-Z0-9]+"},{cN:"built_in",bK:"ComSpec Clipboard ClipboardAll ErrorLevel"}];return{cI:true,k:{keyword:"Break Continue Else Gosub If Loop Return While",literal:"A true false NOT AND OR"},c:a.concat([d,b.inherit(b.QSM,{c:[d]}),c,{cN:"number",b:b.NR,r:0},{cN:"var_expand",b:"%",e:"%",i:"\\n",c:[d]},{cN:"label",c:[d],v:[{b:'^[^\\n";]+::(?!=)'},{b:'^[^\\n";]+:(?!=)',r:0}]},{b:",\\s*,",r:10}])}});hljs.registerLanguage("avrasm",function(a){return{cI:true,l:"\\.?"+a.IR,k:{keyword:"adc add adiw and andi asr bclr bld brbc brbs brcc brcs break breq brge brhc brhs brid brie brlo brlt brmi brne brpl brsh brtc brts brvc brvs bset bst call cbi cbr clc clh cli cln clr cls clt clv clz com cp cpc cpi cpse dec eicall eijmp elpm eor fmul fmuls fmulsu icall ijmp in inc jmp ld ldd ldi lds lpm lsl lsr mov movw mul muls mulsu neg nop or ori out pop push rcall ret reti rjmp rol ror sbc sbr sbrc sbrs sec seh sbi sbci sbic sbis sbiw sei sen ser ses set sev sez sleep spm st std sts sub subi swap tst wdr",built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 r16 r17 r18 r19 r20 r21 r22 r23 r24 r25 r26 r27 r28 r29 r30 r31 x|0 xh xl y|0 yh yl z|0 zh zl ucsr1c udr1 ucsr1a ucsr1b ubrr1l ubrr1h ucsr0c ubrr0h tccr3c tccr3a tccr3b tcnt3h tcnt3l ocr3ah ocr3al ocr3bh ocr3bl ocr3ch ocr3cl icr3h icr3l etimsk etifr tccr1c ocr1ch ocr1cl twcr twdr twar twsr twbr osccal xmcra xmcrb eicra spmcsr spmcr portg ddrg ping portf ddrf sreg sph spl xdiv rampz eicrb eimsk gimsk gicr eifr gifr timsk tifr mcucr mcucsr tccr0 tcnt0 ocr0 assr tccr1a tccr1b tcnt1h tcnt1l ocr1ah ocr1al ocr1bh ocr1bl icr1h icr1l tccr2 tcnt2 ocr2 ocdr wdtcr sfior eearh eearl eedr eecr porta ddra pina portb ddrb pinb portc ddrc pinc portd ddrd pind spdr spsr spcr udr0 ucsr0a ucsr0b ubrr0l acsr admux adcsr adch adcl porte ddre pine pinf",preprocessor:".byte .cseg .db .def .device .dseg .dw .endmacro .equ .eseg .exit .include .list .listmac .macro .nolist .org .set"},c:[a.CBCM,{cN:"comment",b:";",e:"$",r:0},a.CNM,a.BNM,{cN:"number",b:"\\b(\\$[a-zA-Z0-9]+|0o[0-7]+)"},a.QSM,{cN:"string",b:"'",e:"[^\\\\]'",i:"[^\\\\][^']"},{cN:"label",b:"^[A-Za-z0-9_.$]+:"},{cN:"preprocessor",b:"#",e:"$"},{cN:"localvars",b:"@[0-9]+"}]}});hljs.registerLanguage("axapta",function(a){return{k:"false int abstract private char boolean static null if for true while long throw finally protected final return void enum else break new catch byte super case short default double public try this switch continue reverse firstfast firstonly forupdate nofetch sum avg minof maxof count order group by asc desc index hint like dispaly edit client server ttsbegin ttscommit str real date container anytype common div mod",c:[a.CLCM,a.CBCM,a.ASM,a.QSM,a.CNM,{cN:"preprocessor",b:"#",e:"$"},{cN:"class",bK:"class interface",e:"{",eE:true,i:":",c:[{bK:"extends implements"},a.UTM]}]}});hljs.registerLanguage("bash",function(b){var a={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)\}/}]};var d={cN:"string",b:/"/,e:/"/,c:[b.BE,a,{cN:"variable",b:/\$\(/,e:/\)/,c:[b.BE]}]};var c={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/-?[a-z\.]+/,k:{keyword:"if then else elif fi for break continue while in do done exit return set declare case esac export exec",literal:"true false",built_in:"printf echo read cd pwd pushd popd dirs let eval unset typeset readonly getopts source shopt caller type hash bind help sudo",operator:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"shebang",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:true,c:[b.inherit(b.TM,{b:/\w[\w\d_]*/})],r:0},b.HCM,b.NM,d,c,a]}});hljs.registerLanguage("brainfuck",function(b){var a={cN:"literal",b:"[\\+\\-]",r:0};return{aliases:["bf"],c:[{cN:"comment",b:"[^\\[\\]\\.,\\+\\-<> \r\n]",rE:true,e:"[\\[\\]\\.,\\+\\-<> \r\n]",r:0},{cN:"title",b:"[\\[\\]]",r:0},{cN:"string",b:"[\\.,]",r:0},{b:/\+\+|\-\-/,rB:true,c:[a]},a]}});hljs.registerLanguage("capnproto",function(a){return{aliases:["capnp"],k:{keyword:"struct enum interface union group import using const annotation extends in of on as with from fixed",built_in:"Void Bool Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 Float32 Float64 Text Data AnyPointer AnyStruct Capability List",literal:"true false"},c:[a.QSM,a.NM,a.HCM,{cN:"shebang",b:/@0x[\w\d]{16};/,i:/\n/},{cN:"number",b:/@\d+\b/},{cN:"class",bK:"struct enum",e:/\{/,i:/\n/,c:[a.inherit(a.TM,{starts:{eW:true,eE:true}})]},{cN:"class",bK:"interface",e:/\{/,i:/\n/,c:[a.inherit(a.TM,{starts:{eW:true,eE:true}})]}]}});hljs.registerLanguage("clojure",function(j){var e={built_in:"def cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"};var f="[a-zA-Z_0-9\\!\\.\\?\\-\\+\\*\\/\\<\\=\\>\\&\\#\\$';]+";var a="[\\s:\\(\\{]+\\d+(\\.\\d+)?";var d={cN:"number",b:a,r:0};var i=j.inherit(j.QSM,{i:null});var n={cN:"comment",b:";",e:"$",r:0};var m={cN:"collection",b:"[\\[\\{]",e:"[\\]\\}]"};var c={cN:"comment",b:"\\^"+f};var b={cN:"comment",b:"\\^\\{",e:"\\}"};var h={cN:"attribute",b:"[:]"+f};var l={cN:"list",b:"\\(",e:"\\)"};var g={eW:true,k:{literal:"true false nil"},r:0};var o={k:e,l:f,cN:"keyword",b:f,starts:g};l.c=[{cN:"comment",b:"comment"},o,g];g.c=[l,i,c,b,n,h,m,d];m.c=[l,i,c,n,h,m,d];return{aliases:["clj"],i:/\S/,c:[n,l,{cN:"prompt",b:/^=> /,starts:{e:/\n\n|\Z/}}]}});hljs.registerLanguage("cmake",function(a){return{aliases:["cmake.in"],cI:true,k:{keyword:"add_custom_command add_custom_target add_definitions add_dependencies add_executable add_library add_subdirectory add_test aux_source_directory break build_command cmake_minimum_required cmake_policy configure_file create_test_sourcelist define_property else elseif enable_language enable_testing endforeach endfunction endif endmacro endwhile execute_process export find_file find_library find_package find_path find_program fltk_wrap_ui foreach function get_cmake_property get_directory_property get_filename_component get_property get_source_file_property get_target_property get_test_property if include include_directories include_external_msproject include_regular_expression install link_directories load_cache load_command macro mark_as_advanced message option output_required_files project qt_wrap_cpp qt_wrap_ui remove_definitions return separate_arguments set set_directory_properties set_property set_source_files_properties set_target_properties set_tests_properties site_name source_group string target_link_libraries try_compile try_run unset variable_watch while build_name exec_program export_library_dependencies install_files install_programs install_targets link_libraries make_directory remove subdir_depends subdirs use_mangled_mesa utility_source variable_requires write_file qt5_use_modules qt5_use_package qt5_wrap_cpp on off true false and or",operator:"equal less greater strless strgreater strequal matches"},c:[{cN:"envvar",b:"\\${",e:"}"},a.HCM,a.QSM,a.NM]}});hljs.registerLanguage("coffeescript",function(c){var b={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",reserved:"case default function var void with const let enum export import native __hasProp __extends __slice __bind __indexOf",built_in:"npm require console print module global window document"};var a="[A-Za-z$_][0-9A-Za-z$_]*";var f=c.inherit(c.TM,{b:a});var e={cN:"subst",b:/#\{/,e:/}/,k:b};var d=[c.BNM,c.inherit(c.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[c.BE]},{b:/'/,e:/'/,c:[c.BE]},{b:/"""/,e:/"""/,c:[c.BE,e]},{b:/"/,e:/"/,c:[c.BE,e]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[e,c.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{cN:"property",b:"@"+a},{b:"`",e:"`",eB:true,eE:true,sL:"javascript"}];e.c=d;return{aliases:["coffee","cson","iced"],k:b,i:/\/\*/,c:d.concat([{cN:"comment",b:"###",e:"###"},c.HCM,{cN:"function",b:"(^\\s*|\\B)("+a+"\\s*=\\s*)?(\\(.*\\))?\\s*\\B[-=]>",e:"[-=]>",rB:true,c:[f,{cN:"params",b:"\\([^\\(]",rB:true,c:[{b:/\(/,e:/\)/,k:b,c:["self"].concat(d)}]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:true,i:/[:="\[\]]/,c:[f]},f]},{cN:"attribute",b:a+":",e:":",rB:true,eE:true,r:0}])}});hljs.registerLanguage("cpp",function(a){var b={keyword:"false int float while private char catch export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const struct for static_cast|10 union namespace unsigned long throw volatile static protected bool template mutable if public friend do return goto auto void enum else break new extern using true class asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue wchar_t inline delete alignof char16_t char32_t constexpr decltype noexcept nullptr static_assert thread_local restrict _Bool complex _Complex _Imaginary",built_in:"std string cin cout cerr clog stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf"};return{aliases:["c","h","c++","h++"],k:b,i:"</",c:[a.CLCM,a.CBCM,a.QSM,{cN:"string",b:"'\\\\?.",e:"'",i:"."},{cN:"number",b:"\\b(\\d+(\\.\\d*)?|\\.\\d+)(u|U|l|L|ul|UL|f|F)"},a.CNM,{cN:"preprocessor",b:"#",e:"$",k:"if else elif endif define undef warning error line pragma",c:[{b:'include\\s*[<"]',e:'[>"]',k:"include",i:"\\n"},a.CLCM]},{cN:"stl_container",b:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",e:">",k:b,c:["self"]},{b:a.IR+"::"}]}});hljs.registerLanguage("cs",function(c){var b="abstract as base bool break byte case catch char checked const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual volatile void while async await protected public private internal ascending descending from get group into join let orderby partial select set value var where yield";var a=c.IR+"(<"+c.IR+">)?";return{aliases:["csharp"],k:b,i:/::/,c:[{cN:"comment",b:"///",e:"$",rB:true,c:[{cN:"xmlDocTag",v:[{b:"///",r:0},{b:"<!--|-->"},{b:"</?",e:">"}]}]},c.CLCM,c.CBCM,{cN:"preprocessor",b:"#",e:"$",k:"if else elif endif define undef warning error line region endregion pragma checksum"},{cN:"string",b:'@"',e:'"',c:[{b:'""'}]},c.ASM,c.QSM,c.CNM,{bK:"class namespace interface",e:/[{;=]/,i:/[^\s:]/,c:[c.TM,c.CLCM,c.CBCM]},{bK:"new",e:/\s/,r:0},{cN:"function",b:"("+a+"\\s+)+"+c.IR+"\\s*\\(",rB:true,e:/[{;=]/,eE:true,k:b,c:[{b:c.IR+"\\s*\\(",rB:true,c:[c.TM]},{cN:"params",b:/\(/,e:/\)/,k:b,c:[c.ASM,c.QSM,c.CNM,c.CBCM]},c.CLCM,c.CBCM]}]}});hljs.registerLanguage("css",function(a){var b="[a-zA-Z-][a-zA-Z0-9_-]*";var c={cN:"function",b:b+"\\(",rB:true,eE:true,e:"\\("};return{cI:true,i:"[=/|']",c:[a.CBCM,{cN:"id",b:"\\#[A-Za-z0-9_-]+"},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"pseudo",b:":(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\\\"\\']+"},{cN:"at_rule",b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{cN:"at_rule",b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:true,eE:true,r:0,c:[c,a.ASM,a.QSM,a.CSSNM]}]},{cN:"tag",b:b,r:0},{cN:"rules",b:"{",e:"}",i:"[^\\s]",r:0,c:[a.CBCM,{cN:"rule",b:"[^\\s]",rB:true,e:";",eW:true,c:[{cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:true,i:"[^\\s]",starts:{cN:"value",eW:true,eE:true,c:[c,a.CSSNM,a.QSM,a.ASM,a.CBCM,{cN:"hexcolor",b:"#[0-9A-Fa-f]+"},{cN:"important",b:"!important"}]}}]}]}]}});hljs.registerLanguage("d",function(x){var b={keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",built_in:"bool cdouble cent cfloat char creal dchar delegate double dstring float function idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar wstring",literal:"false null true"};var c="(0|[1-9][\\d_]*)",q="(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)",h="0[bB][01_]+",v="([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)",y="0[xX]"+v,p="([eE][+-]?"+q+")",o="("+q+"(\\.\\d*|"+p+")|\\d+\\."+q+q+"|\\."+c+p+"?)",k="(0[xX]("+v+"\\."+v+"|\\.?"+v+")[pP][+-]?"+q+")",l="("+c+"|"+h+"|"+y+")",n="("+k+"|"+o+")";var z="\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};";var m={cN:"number",b:"\\b"+l+"(L|u|U|Lu|LU|uL|UL)?",r:0};var j={cN:"number",b:"\\b("+n+"([fF]|L|i|[fF]i|Li)?|"+l+"(i|[fF]i|Li))",r:0};var s={cN:"string",b:"'("+z+"|.)",e:"'",i:"."};var r={b:z,r:0};var w={cN:"string",b:'"',c:[r],e:'"[cwd]?'};var f={cN:"string",b:'[rq]"',e:'"[cwd]?',r:5};var u={cN:"string",b:"`",e:"`[cwd]?"};var i={cN:"string",b:'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?',r:10};var t={cN:"string",b:'q"\\{',e:'\\}"'};var e={cN:"shebang",b:"^#!",e:"$",r:5};var g={cN:"preprocessor",b:"#(line)",e:"$",r:5};var d={cN:"keyword",b:"@[a-zA-Z_][a-zA-Z_\\d]*"};var a={cN:"comment",b:"\\/\\+",c:["self"],e:"\\+\\/",r:10};return{l:x.UIR,k:b,c:[x.CLCM,x.CBCM,a,i,w,f,u,t,j,m,s,e,g,d]}});hljs.registerLanguage("markdown",function(a){return{aliases:["md","mkdown","mkd"],c:[{cN:"header",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"blockquote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"`.+?`"},{b:"^( {4}|\t)",e:"$",r:0}]},{cN:"horizontal_rule",b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:true,c:[{cN:"link_label",b:"\\[",e:"\\]",eB:true,rE:true,r:0},{cN:"link_url",b:"\\]\\(",e:"\\)",eB:true,eE:true},{cN:"link_reference",b:"\\]\\[",e:"\\]",eB:true,eE:true}],r:10},{b:"^\\[.+\\]:",rB:true,c:[{cN:"link_reference",b:"\\[",e:"\\]:",eB:true,eE:true,starts:{cN:"link_url",e:"$"}}]}]}});hljs.registerLanguage("dart",function(b){var d={cN:"subst",b:"\\$\\{",e:"}",k:"true false null this is new super"};var c={cN:"string",v:[{b:"r'''",e:"'''"},{b:'r"""',e:'"""'},{b:"r'",e:"'",i:"\\n"},{b:'r"',e:'"',i:"\\n"},{b:"'''",e:"'''",c:[b.BE,d]},{b:'"""',e:'"""',c:[b.BE,d]},{b:"'",e:"'",i:"\\n",c:[b.BE,d]},{b:'"',e:'"',i:"\\n",c:[b.BE,d]}]};d.c=[b.CNM,c];var a={keyword:"assert break case catch class const continue default do else enum extends false final finally for if in is new null rethrow return super switch this throw true try var void while with",literal:"abstract as dynamic export external factory get implements import library operator part set static typedef",built_in:"print Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set Stopwatch String StringBuffer StringSink Symbol Type Uri bool double int num document window querySelector querySelectorAll Element ElementList"};return{k:a,c:[c,{cN:"dartdoc",b:"/\\*\\*",e:"\\*/",sL:"markdown",subLanguageMode:"continuous"},{cN:"dartdoc",b:"///",e:"$",sL:"markdown",subLanguageMode:"continuous"},b.CLCM,b.CBCM,{cN:"class",bK:"class interface",e:"{",eE:true,c:[{bK:"extends implements"},b.UTM]},b.CNM,{cN:"annotation",b:"@[A-Za-z]+"},{b:"=>"}]}});hljs.registerLanguage("delphi",function(b){var a="exports register file shl array record property for mod while set ally label uses raise not stored class safecall var interface or private static exit index inherited to else stdcall override shr asm far resourcestring finalization packed virtual out and protected library do xorwrite goto near function end div overload object unit begin string on inline repeat until destructor write message program with read initialization except default nil if case cdecl in downto threadvar of try pascal const external constructor type public then implementation finally published procedure";var e={cN:"comment",v:[{b:/\{/,e:/\}/,r:0},{b:/\(\*/,e:/\*\)/,r:10}]};var c={cN:"string",b:/'/,e:/'/,c:[{b:/''/}]};var d={cN:"string",b:/(#\d+)+/};var f={b:b.IR+"\\s*=\\s*class\\s*\\(",rB:true,c:[b.TM]};var g={cN:"function",bK:"function constructor destructor procedure",e:/[:;]/,k:"function constructor|10 destructor|10 procedure|10",c:[b.TM,{cN:"params",b:/\(/,e:/\)/,k:a,c:[c,d]},e]};return{cI:true,k:a,i:/("|\$[G-Zg-z]|\/\*|<\/)/,c:[e,b.CLCM,c,d,b.NM,f,g]}});hljs.registerLanguage("diff",function(a){return{aliases:["patch"],c:[{cN:"chunk",r:10,v:[{b:/^\@\@ +\-\d+,\d+ +\+\d+,\d+ +\@\@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"header",v:[{b:/Index: /,e:/$/},{b:/=====/,e:/=====$/},{b:/^\-\-\-/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+\+\+/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"change",b:"^\\!",e:"$"}]}});hljs.registerLanguage("django",function(a){var b={cN:"filter",b:/\|[A-Za-z]+\:?/,k:"truncatewords removetags linebreaksbr yesno get_digit timesince random striptags filesizeformat escape linebreaks length_is ljust rjust cut urlize fix_ampersands title floatformat capfirst pprint divisibleby add make_list unordered_list urlencode timeuntil urlizetrunc wordcount stringformat linenumbers slice date dictsort dictsortreversed default_if_none pluralize lower join center default truncatewords_html upper length phone2numeric wordwrap time addslashes slugify first escapejs force_escape iriencode last safe safeseq truncatechars localize unlocalize localtime utc timezone",c:[{cN:"argument",b:/"/,e:/"/},{cN:"argument",b:/'/,e:/'/}]};return{aliases:["jinja"],cI:true,sL:"xml",subLanguageMode:"continuous",c:[{cN:"template_comment",b:/\{%\s*comment\s*%}/,e:/\{%\s*endcomment\s*%}/},{cN:"template_comment",b:/\{#/,e:/#}/},{cN:"template_tag",b:/\{%/,e:/%}/,k:"comment endcomment load templatetag ifchanged endifchanged if endif firstof for endfor in ifnotequal endifnotequal widthratio extends include spaceless endspaceless regroup by as ifequal endifequal ssi now with cycle url filter endfilter debug block endblock else autoescape endautoescape csrf_token empty elif endwith static trans blocktrans endblocktrans get_static_prefix get_media_prefix plural get_current_language language get_available_languages get_current_language_bidi get_language_info get_language_info_list localize endlocalize localtime endlocaltime timezone endtimezone get_current_timezone verbatim",c:[b]},{cN:"variable",b:/\{\{/,e:/}}/,c:[b]}]}});hljs.registerLanguage("dos",function(a){var c={cN:"comment",b:/@?rem\b/,e:/$/,r:10};var b={cN:"label",b:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)",r:0};return{aliases:["bat","cmd"],cI:true,k:{flow:"if else goto for in do call exit not exist errorlevel defined",operator:"equ neq lss leq gtr geq",keyword:"shift cd dir echo setlocal endlocal set pause copy",stream:"prn nul lpt3 lpt2 lpt1 con com4 com3 com2 com1 aux",winutils:"ping net ipconfig taskkill xcopy ren del",built_in:"append assoc at attrib break cacls cd chcp chdir chkdsk chkntfs cls cmd color comp compact convert date dir diskcomp diskcopy doskey erase fs find findstr format ftype graftabl help keyb label md mkdir mode more move path pause print popd pushd promt rd recover rem rename replace restore rmdir shiftsort start subst time title tree type ver verify vol",},c:[{cN:"envvar",b:/%%[^ ]|%[^ ]+?%|![^ ]+?!/},{cN:"function",b:b.b,e:"goto:eof",c:[a.inherit(a.TM,{b:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),c]},{cN:"number",b:"\\b\\d+",r:0},c]}});hljs.registerLanguage("dust",function(b){var a="if eq ne lt lte gt gte select default math sep";return{aliases:["dst"],cI:true,sL:"xml",subLanguageMode:"continuous",c:[{cN:"expression",b:"{",e:"}",r:0,c:[{cN:"begin-block",b:"#[a-zA-Z- .]+",k:a},{cN:"string",b:'"',e:'"'},{cN:"end-block",b:"\\/[a-zA-Z- .]+",k:a},{cN:"variable",b:"[a-zA-Z-.]+",k:a,r:0}]}]}});hljs.registerLanguage("elixir",function(e){var f="[a-zA-Z_][a-zA-Z0-9_]*(\\!|\\?)?";var g="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?";var i="and false then defined module in return redo retry end for true self when next until do begin unless nil break not case cond alias while ensure or include use alias fn quote";var c={cN:"subst",b:"#\\{",e:"}",l:f,k:i};var d={cN:"string",c:[e.BE,c],v:[{b:/'/,e:/'/},{b:/"/,e:/"/}]};var b={eW:true,rE:true,l:f,k:i,r:0};var h={cN:"function",bK:"def defmacro",e:/\bdo\b/,c:[e.inherit(e.TM,{b:g,starts:b})]};var j=e.inherit(h,{cN:"class",bK:"defmodule defrecord",e:/\bdo\b|$|;/});var a=[d,e.HCM,j,h,{cN:"constant",b:"(\\b[A-Z_]\\w*(.)?)+",r:0},{cN:"symbol",b:":",c:[d,{b:g}],r:0},{cN:"symbol",b:f+":",r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{cN:"variable",b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{b:"->"},{b:"("+e.RSR+")\\s*",c:[e.HCM,{cN:"regexp",i:"\\n",c:[e.BE,c],v:[{b:"/",e:"/[a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}],r:0}];c.c=a;b.c=a;return{l:f,k:i,c:a}});hljs.registerLanguage("erlang-repl",function(a){return{k:{special_functions:"spawn spawn_link self",reserved:"after and andalso|10 band begin bnot bor bsl bsr bxor case catch cond div end fun if let not of or orelse|10 query receive rem try when xor"},c:[{cN:"prompt",b:"^[0-9]+> ",r:10},{cN:"comment",b:"%",e:"$"},{cN:"number",b:"\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)",r:0},a.ASM,a.QSM,{cN:"constant",b:"\\?(::)?([A-Z]\\w*(::)?)+"},{cN:"arrow",b:"->"},{cN:"ok",b:"ok"},{cN:"exclamation_mark",b:"!"},{cN:"function_or_atom",b:"(\\b[a-z'][a-zA-Z0-9_']*:[a-z'][a-zA-Z0-9_']*)|(\\b[a-z'][a-zA-Z0-9_']*)",r:0},{cN:"variable",b:"[A-Z][a-zA-Z0-9_']*",r:0}]}});hljs.registerLanguage("erlang",function(i){var c="[a-z'][a-zA-Z0-9_']*";var o="("+c+":"+c+"|"+c+")";var f={keyword:"after and andalso|10 band begin bnot bor bsl bzr bxor case catch cond div end fun if let not of orelse|10 query receive rem try when xor",literal:"false true"};var l={cN:"comment",b:"%",e:"$"};var e={cN:"number",b:"\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)",r:0};var g={b:"fun\\s+"+c+"/\\d+"};var n={b:o+"\\(",e:"\\)",rB:true,r:0,c:[{cN:"function_name",b:o,r:0},{b:"\\(",e:"\\)",eW:true,rE:true,r:0}]};var h={cN:"tuple",b:"{",e:"}",r:0};var a={cN:"variable",b:"\\b_([A-Z][A-Za-z0-9_]*)?",r:0};var m={cN:"variable",b:"[A-Z][a-zA-Z0-9_]*",r:0};var b={b:"#"+i.UIR,r:0,rB:true,c:[{cN:"record_name",b:"#"+i.UIR,r:0},{b:"{",e:"}",r:0}]};var k={bK:"fun receive if try case",e:"end",k:f};k.c=[l,g,i.inherit(i.ASM,{cN:""}),k,n,i.QSM,e,h,a,m,b];var j=[l,g,k,n,i.QSM,e,h,a,m,b];n.c[1].c=j;h.c=j;b.c[1].c=j;var d={cN:"params",b:"\\(",e:"\\)",c:j};return{aliases:["erl"],k:f,i:"(</|\\*=|\\+=|-=|/\\*|\\*/|\\(\\*|\\*\\))",c:[{cN:"function",b:"^"+c+"\\s*\\(",e:"->",rB:true,i:"\\(|#|//|/\\*|\\\\|:|;",c:[d,i.inherit(i.TM,{b:c})],starts:{e:";|\\.",k:f,c:j}},l,{cN:"pp",b:"^-",e:"\\.",r:0,eE:true,rB:true,l:"-"+i.IR,k:"-module -record -undef -export -ifdef -ifndef -author -copyright -doc -vsn -import -include -include_lib -compile -define -else -endif -file -behaviour -behavior -spec",c:[d]},e,i.QSM,b,a,m,h,{b:/\.$/}]}});hljs.registerLanguage("fix",function(a){return{c:[{b:/[^\u2401\u0001]+/,e:/[\u2401\u0001]/,eE:true,rB:true,rE:false,c:[{b:/([^\u2401\u0001=]+)/,e:/=([^\u2401\u0001=]+)/,rE:true,rB:false,cN:"attribute"},{b:/=/,e:/([\u2401\u0001])/,eE:true,eB:true,cN:"string"}]}],cI:true}});hljs.registerLanguage("fsharp",function(a){var b={b:"<",e:">",c:[a.inherit(a.TM,{b:/'[a-zA-Z0-9_]+/})]};return{aliases:["fs"],k:"yield! return! let! do!abstract and as assert base begin class default delegate do done downcast downto elif else end exception extern false finally for fun function global if in inherit inline interface internal lazy let match member module mutable namespace new null of open or override private public rec return sig static struct then to true try type upcast use val void when while with yield",c:[{cN:"string",b:'@"',e:'"',c:[{b:'""'}]},{cN:"string",b:'"""',e:'"""'},{cN:"comment",b:"\\(\\*",e:"\\*\\)"},{cN:"class",bK:"type",e:"\\(|=|$",eE:true,c:[a.UTM,b]},{cN:"annotation",b:"\\[<",e:">\\]",r:10},{cN:"attribute",b:"\\B('[A-Za-z])\\b",c:[a.BE]},a.CLCM,a.inherit(a.QSM,{i:null}),a.CNM]}});hljs.registerLanguage("gcode",function(a){var e="[A-Z_][A-Z0-9_.]*";var f="\\%";var c={literal:"",built_in:"",keyword:"IF DO WHILE ENDWHILE CALL ENDIF SUB ENDSUB GOTO REPEAT ENDREPEAT EQ LT GT NE GE LE OR XOR"};var b={cN:"preprocessor",b:"([O])([0-9]+)"};var d=[a.CLCM,{cN:"comment",b:/\(/,e:/\)/,c:[a.PWM]},a.CBCM,a.inherit(a.CNM,{b:"([-+]?([0-9]*\\.?[0-9]+\\.?))|"+a.CNR}),a.inherit(a.ASM,{i:null}),a.inherit(a.QSM,{i:null}),{cN:"keyword",b:"([G])([0-9]+\\.?[0-9]?)"},{cN:"title",b:"([M])([0-9]+\\.?[0-9]?)"},{cN:"title",b:"(VC|VS|#)",e:"(\\d+)"},{cN:"title",b:"(VZOFX|VZOFY|VZOFZ)"},{cN:"built_in",b:"(ATAN|ABS|ACOS|ASIN|SIN|COS|EXP|FIX|FUP|ROUND|LN|TAN)(\\[)",e:"([-+]?([0-9]*\\.?[0-9]+\\.?))(\\])"},{cN:"label",v:[{b:"N",e:"\\d+",i:"\\W"}]}];return{aliases:["nc"],cI:true,l:e,k:c,c:[{cN:"preprocessor",b:f},b].concat(d)}});hljs.registerLanguage("gherkin",function(a){return{aliases:["feature"],k:"Feature Background Ability Business Need Scenario Scenarios Scenario Outline Scenario Template Examples Given And Then But When",c:[{cN:"keyword",b:"\\*"},{cN:"comment",b:"@[^@\r\n\t ]+",e:"$"},{cN:"string",b:"\\|",e:"\\$"},{cN:"variable",b:"<",e:">",},a.HCM,{cN:"string",b:'"""',e:'"""'},a.QSM]}});hljs.registerLanguage("glsl",function(a){return{k:{keyword:"atomic_uint attribute bool break bvec2 bvec3 bvec4 case centroid coherent const continue default discard dmat2 dmat2x2 dmat2x3 dmat2x4 dmat3 dmat3x2 dmat3x3 dmat3x4 dmat4 dmat4x2 dmat4x3 dmat4x4 do double dvec2 dvec3 dvec4 else flat float for highp if iimage1D iimage1DArray iimage2D iimage2DArray iimage2DMS iimage2DMSArray iimage2DRect iimage3D iimageBuffer iimageCube iimageCubeArray image1D image1DArray image2D image2DArray image2DMS image2DMSArray image2DRect image3D imageBuffer imageCube imageCubeArray in inout int invariant isampler1D isampler1DArray isampler2D isampler2DArray isampler2DMS isampler2DMSArray isampler2DRect isampler3D isamplerBuffer isamplerCube isamplerCubeArray ivec2 ivec3 ivec4 layout lowp mat2 mat2x2 mat2x3 mat2x4 mat3 mat3x2 mat3x3 mat3x4 mat4 mat4x2 mat4x3 mat4x4 mediump noperspective out patch precision readonly restrict return sample sampler1D sampler1DArray sampler1DArrayShadow sampler1DShadow sampler2D sampler2DArray sampler2DArrayShadow sampler2DMS sampler2DMSArray sampler2DRect sampler2DRectShadow sampler2DShadow sampler3D samplerBuffer samplerCube samplerCubeArray samplerCubeArrayShadow samplerCubeShadow smooth struct subroutine switch uimage1D uimage1DArray uimage2D uimage2DArray uimage2DMS uimage2DMSArray uimage2DRect uimage3D uimageBuffer uimageCube uimageCubeArray uint uniform usampler1D usampler1DArray usampler2D usampler2DArray usampler2DMS usampler2DMSArray usampler2DRect usampler3D usamplerBuffer usamplerCube usamplerCubeArray uvec2 uvec3 uvec4 varying vec2 vec3 vec4 void volatile while writeonly",built_in:"gl_BackColor gl_BackLightModelProduct gl_BackLightProduct gl_BackMaterial gl_BackSecondaryColor gl_ClipDistance gl_ClipPlane gl_ClipVertex gl_Color gl_DepthRange gl_EyePlaneQ gl_EyePlaneR gl_EyePlaneS gl_EyePlaneT gl_Fog gl_FogCoord gl_FogFragCoord gl_FragColor gl_FragCoord gl_FragData gl_FragDepth gl_FrontColor gl_FrontFacing gl_FrontLightModelProduct gl_FrontLightProduct gl_FrontMaterial gl_FrontSecondaryColor gl_InstanceID gl_InvocationID gl_Layer gl_LightModel gl_LightSource gl_MaxAtomicCounterBindings gl_MaxAtomicCounterBufferSize gl_MaxClipDistances gl_MaxClipPlanes gl_MaxCombinedAtomicCounterBuffers gl_MaxCombinedAtomicCounters gl_MaxCombinedImageUniforms gl_MaxCombinedImageUnitsAndFragmentOutputs gl_MaxCombinedTextureImageUnits gl_MaxDrawBuffers gl_MaxFragmentAtomicCounterBuffers gl_MaxFragmentAtomicCounters gl_MaxFragmentImageUniforms gl_MaxFragmentInputComponents gl_MaxFragmentUniformComponents gl_MaxFragmentUniformVectors gl_MaxGeometryAtomicCounterBuffers gl_MaxGeometryAtomicCounters gl_MaxGeometryImageUniforms gl_MaxGeometryInputComponents gl_MaxGeometryOutputComponents gl_MaxGeometryOutputVertices gl_MaxGeometryTextureImageUnits gl_MaxGeometryTotalOutputComponents gl_MaxGeometryUniformComponents gl_MaxGeometryVaryingComponents gl_MaxImageSamples gl_MaxImageUnits gl_MaxLights gl_MaxPatchVertices gl_MaxProgramTexelOffset gl_MaxTessControlAtomicCounterBuffers gl_MaxTessControlAtomicCounters gl_MaxTessControlImageUniforms gl_MaxTessControlInputComponents gl_MaxTessControlOutputComponents gl_MaxTessControlTextureImageUnits gl_MaxTessControlTotalOutputComponents gl_MaxTessControlUniformComponents gl_MaxTessEvaluationAtomicCounterBuffers gl_MaxTessEvaluationAtomicCounters gl_MaxTessEvaluationImageUniforms gl_MaxTessEvaluationInputComponents gl_MaxTessEvaluationOutputComponents gl_MaxTessEvaluationTextureImageUnits gl_MaxTessEvaluationUniformComponents gl_MaxTessGenLevel gl_MaxTessPatchComponents gl_MaxTextureCoords gl_MaxTextureImageUnits gl_MaxTextureUnits gl_MaxVaryingComponents gl_MaxVaryingFloats gl_MaxVaryingVectors gl_MaxVertexAtomicCounterBuffers gl_MaxVertexAtomicCounters gl_MaxVertexAttribs gl_MaxVertexImageUniforms gl_MaxVertexOutputComponents gl_MaxVertexTextureImageUnits gl_MaxVertexUniformComponents gl_MaxVertexUniformVectors gl_MaxViewports gl_MinProgramTexelOffsetgl_ModelViewMatrix gl_ModelViewMatrixInverse gl_ModelViewMatrixInverseTranspose gl_ModelViewMatrixTranspose gl_ModelViewProjectionMatrix gl_ModelViewProjectionMatrixInverse gl_ModelViewProjectionMatrixInverseTranspose gl_ModelViewProjectionMatrixTranspose gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 gl_Normal gl_NormalMatrix gl_NormalScale gl_ObjectPlaneQ gl_ObjectPlaneR gl_ObjectPlaneS gl_ObjectPlaneT gl_PatchVerticesIn gl_PerVertex gl_Point gl_PointCoord gl_PointSize gl_Position gl_PrimitiveID gl_PrimitiveIDIn gl_ProjectionMatrix gl_ProjectionMatrixInverse gl_ProjectionMatrixInverseTranspose gl_ProjectionMatrixTranspose gl_SampleID gl_SampleMask gl_SampleMaskIn gl_SamplePosition gl_SecondaryColor gl_TessCoord gl_TessLevelInner gl_TessLevelOuter gl_TexCoord gl_TextureEnvColor gl_TextureMatrixInverseTranspose gl_TextureMatrixTranspose gl_Vertex gl_VertexID gl_ViewportIndex gl_in gl_out EmitStreamVertex EmitVertex EndPrimitive EndStreamPrimitive abs acos acosh all any asin asinh atan atanh atomicCounter atomicCounterDecrement atomicCounterIncrement barrier bitCount bitfieldExtract bitfieldInsert bitfieldReverse ceil clamp cos cosh cross dFdx dFdy degrees determinant distance dot equal exp exp2 faceforward findLSB findMSB floatBitsToInt floatBitsToUint floor fma fract frexp ftransform fwidth greaterThan greaterThanEqual imageAtomicAdd imageAtomicAnd imageAtomicCompSwap imageAtomicExchange imageAtomicMax imageAtomicMin imageAtomicOr imageAtomicXor imageLoad imageStore imulExtended intBitsToFloat interpolateAtCentroid interpolateAtOffset interpolateAtSample inverse inversesqrt isinf isnan ldexp length lessThan lessThanEqual log log2 matrixCompMult max memoryBarrier min mix mod modf noise1 noise2 noise3 noise4 normalize not notEqual outerProduct packDouble2x32 packHalf2x16 packSnorm2x16 packSnorm4x8 packUnorm2x16 packUnorm4x8 pow radians reflect refract round roundEven shadow1D shadow1DLod shadow1DProj shadow1DProjLod shadow2D shadow2DLod shadow2DProj shadow2DProjLod sign sin sinh smoothstep sqrt step tan tanh texelFetch texelFetchOffset texture texture1D texture1DLod texture1DProj texture1DProjLod texture2D texture2DLod texture2DProj texture2DProjLod texture3D texture3DLod texture3DProj texture3DProjLod textureCube textureCubeLod textureGather textureGatherOffset textureGatherOffsets textureGrad textureGradOffset textureLod textureLodOffset textureOffset textureProj textureProjGrad textureProjGradOffset textureProjLod textureProjLodOffset textureProjOffset textureQueryLod textureSize transpose trunc uaddCarry uintBitsToFloat umulExtended unpackDouble2x32 unpackHalf2x16 unpackSnorm2x16 unpackSnorm4x8 unpackUnorm2x16 unpackUnorm4x8 usubBorrow gl_TextureMatrix gl_TextureMatrixInverse",literal:"true false"},i:'"',c:[a.CLCM,a.CBCM,a.CNM,{cN:"preprocessor",b:"#",e:"$"}]}});hljs.registerLanguage("go",function(a){var b={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer",constant:"true false iota nil",typename:"bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{aliases:["golang"],k:b,i:"</",c:[a.CLCM,a.CBCM,a.QSM,{cN:"string",b:"'",e:"[^\\\\]'"},{cN:"string",b:"`",e:"`"},{cN:"number",b:"[^a-zA-Z_0-9](\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s)(\\+|\\-)?\\d+)?",r:0},a.CNM]}});hljs.registerLanguage("gradle",function(a){return{cI:true,k:{keyword:"task project allprojects subprojects artifacts buildscript configurations dependencies repositories sourceSets description delete from into include exclude source classpath destinationDir includes options sourceCompatibility targetCompatibility group flatDir doLast doFirst flatten todir fromdir ant def abstract break case catch continue default do else extends final finally for if implements instanceof native new private protected public return static switch synchronized throw throws transient try volatile while strictfp package import false null super this true antlrtask checkstyle codenarc copy boolean byte char class double float int interface long short void compile runTime file fileTree abs any append asList asWritable call collect compareTo count div dump each eachByte eachFile eachLine every find findAll flatten getAt getErr getIn getOut getText grep immutable inject inspect intersect invokeMethods isCase join leftShift minus multiply newInputStream newOutputStream newPrintWriter newReader newWriter next plus pop power previous print println push putAt read readBytes readLines reverse reverseEach round size sort splitEachLine step subMap times toInteger toList tokenize upto waitForOrKill withPrintWriter withReader withStream withWriter withWriterAppend write writeLine"},c:[a.CLCM,a.CBCM,a.ASM,a.QSM,a.NM,a.RM]}});hljs.registerLanguage("groovy",function(a){return{k:{typename:"byte short char int long boolean float double void",literal:"true false null",keyword:"def as in assert trait super this abstract static volatile transient public private protected synchronized final class interface enum if else for while switch case break default continue throw throws try catch finally implements extends new import package return instanceof"},c:[a.CLCM,{cN:"javadoc",b:"/\\*\\*",e:"\\*//*",c:[{cN:"javadoctag",b:"@[A-Za-z]+"}]},a.CBCM,{cN:"string",b:'"""',e:'"""'},{cN:"string",b:"'''",e:"'''"},{cN:"string",b:"\\$/",e:"/\\$",r:10},a.ASM,{cN:"regexp",b:/~?\/[^\/\n]+\//,c:[a.BE]},a.QSM,{cN:"shebang",b:"^#!/usr/bin/env",e:"$",i:"\n"},a.BNM,{cN:"class",bK:"class interface trait enum",e:"{",i:":",c:[{bK:"extends implements"},a.UTM,]},a.CNM,{cN:"annotation",b:"@[A-Za-z]+"},{cN:"string",b:/[^\?]{0}[A-Za-z0-9_$]+ *:/},{b:/\?/,e:/\:/},{cN:"label",b:"^\\s*[A-Za-z0-9_$]+:"},]}});hljs.registerLanguage("ruby",function(f){var j="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?";var i="and false then defined module in return redo if BEGIN retry end for true self when next until do begin unless END rescue nil else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor";var b={cN:"yardoctag",b:"@[A-Za-z]+"};var c={cN:"value",b:"#<",e:">"};var k={cN:"comment",v:[{b:"#",e:"$",c:[b]},{b:"^\\=begin",e:"^\\=end",c:[b],r:10},{b:"^__END__",e:"\\n$"}]};var d={cN:"subst",b:"#\\{",e:"}",k:i};var e={cN:"string",c:[f.BE,d],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:"%[qw]?\\(",e:"\\)"},{b:"%[qw]?\\[",e:"\\]"},{b:"%[qw]?{",e:"}"},{b:"%[qw]?<",e:">"},{b:"%[qw]?/",e:"/"},{b:"%[qw]?%",e:"%"},{b:"%[qw]?-",e:"-"},{b:"%[qw]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]};var a={cN:"params",b:"\\(",e:"\\)",k:i};var h=[e,c,k,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[f.inherit(f.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{cN:"inheritance",b:"<\\s*",c:[{cN:"parent",b:"("+f.IR+"::)?"+f.IR}]},k]},{cN:"function",bK:"def",e:" |$|;",r:0,c:[f.inherit(f.TM,{b:j}),a,k]},{cN:"constant",b:"(::)?(\\b[A-Z]\\w*(::)?)+",r:0},{cN:"symbol",b:f.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":",c:[e,{b:j}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{cN:"variable",b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{b:"("+f.RSR+")\\s*",c:[c,k,{cN:"regexp",c:[f.BE,d],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}],r:0}];d.c=h;a.c=h;var g=[{b:/^\s*=>/,cN:"status",starts:{e:"$",c:h}},{cN:"prompt",b:/^\S[^=>\n]*>+/,starts:{e:"$",c:h}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:i,c:[k].concat(g).concat(h)}});hljs.registerLanguage("haml",function(a){return{cI:true,c:[{cN:"doctype",b:"^!!!( (5|1\\.1|Strict|Frameset|Basic|Mobile|RDFa|XML\\b.*))?$",r:10},{cN:"comment",b:"^\\s*(!=#|=#|-#|/).*$",r:0},{b:"^\\s*(-|=|!=)(?!#)",starts:{e:"\\n",sL:"ruby"}},{cN:"tag",b:"^\\s*%",c:[{cN:"title",b:"\\w+"},{cN:"value",b:"[#\\.]\\w+"},{b:"{\\s*",e:"\\s*}",eE:true,c:[{b:":\\w+\\s*=>",e:",\\s+",rB:true,eW:true,c:[{cN:"symbol",b:":\\w+"},{cN:"string",b:'"',e:'"'},{cN:"string",b:"'",e:"'"},{b:"\\w+",r:0}]}]},{b:"\\(\\s*",e:"\\s*\\)",eE:true,c:[{b:"\\w+\\s*=",e:"\\s+",rB:true,eW:true,c:[{cN:"attribute",b:"\\w+",r:0},{cN:"string",b:'"',e:'"'},{cN:"string",b:"'",e:"'"},{b:"\\w+",r:0}]}]}]},{cN:"bullet",b:"^\\s*[=~]\\s*",r:0},{b:"#{",starts:{e:"}",sL:"ruby"}}]}});hljs.registerLanguage("handlebars",function(b){var a="each in with if else unless bindattr action collection debugger log outlet template unbound view yield";return{aliases:["hbs","html.hbs","html.handlebars"],cI:true,sL:"xml",subLanguageMode:"continuous",c:[{cN:"expression",b:"{{",e:"}}",c:[{cN:"begin-block",b:"#[a-zA-Z- .]+",k:a},{cN:"string",b:'"',e:'"'},{cN:"end-block",b:"\\/[a-zA-Z- .]+",k:a},{cN:"variable",b:"[a-zA-Z-.]+",k:a}]}]}});hljs.registerLanguage("haskell",function(f){var g={cN:"comment",v:[{b:"--",e:"$"},{b:"{-",e:"-}",c:["self"]}]};var e={cN:"pragma",b:"{-#",e:"#-}"};var b={cN:"preprocessor",b:"^#",e:"$"};var d={cN:"type",b:"\\b[A-Z][\\w']*",r:0};var c={cN:"container",b:"\\(",e:"\\)",i:'"',c:[e,g,b,{cN:"type",b:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},f.inherit(f.TM,{b:"[_a-z][\\w']*"})]};var a={cN:"container",b:"{",e:"}",c:c.c};return{aliases:["hs"],k:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",c:[{cN:"module",b:"\\bmodule\\b",e:"where",k:"module where",c:[c,g],i:"\\W\\.|;"},{cN:"import",b:"\\bimport\\b",e:"$",k:"import|0 qualified as hiding",c:[c,g],i:"\\W\\.|;"},{cN:"class",b:"^(\\s*)?(class|instance)\\b",e:"where",k:"class family instance where",c:[d,c,g]},{cN:"typedef",b:"\\b(data|(new)?type)\\b",e:"$",k:"data family type newtype deriving",c:[e,g,d,c,a]},{cN:"default",bK:"default",e:"$",c:[d,c,g]},{cN:"infix",bK:"infix infixl infixr",e:"$",c:[f.CNM,g]},{cN:"foreign",b:"\\bforeign\\b",e:"$",k:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",c:[d,f.QSM,g]},{cN:"shebang",b:"#!\\/usr\\/bin\\/env runhaskell",e:"$"},e,g,b,f.QSM,f.CNM,d,f.inherit(f.TM,{b:"^[_a-z][\\w']*"}),{b:"->|<-"}]}});hljs.registerLanguage("haxe",function(a){var c="[a-zA-Z_$][a-zA-Z0-9_$]*";var b="([*]|[a-zA-Z_$][a-zA-Z0-9_$]*)";return{aliases:["hx"],k:{keyword:"break callback case cast catch class continue default do dynamic else enum extends extern for function here if implements import in inline interface never new override package private public return static super switch this throw trace try typedef untyped using var while",literal:"true false null"},c:[a.ASM,a.QSM,a.CLCM,a.CBCM,a.CNM,{cN:"class",bK:"class interface",e:"{",eE:true,c:[{bK:"extends implements"},a.TM]},{cN:"preprocessor",b:"#",e:"$",k:"if else elseif end error"},{cN:"function",bK:"function",e:"[{;]",eE:true,i:"\\S",c:[a.TM,{cN:"params",b:"\\(",e:"\\)",c:[a.ASM,a.QSM,a.CLCM,a.CBCM]},{cN:"type",b:":",e:b,r:10}]}]}});hljs.registerLanguage("http",function(a){return{i:"\\S",c:[{cN:"status",b:"^HTTP/[0-9\\.]+",e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{cN:"request",b:"^[A-Z]+ (.*?) HTTP/[0-9\\.]+$",rB:true,e:"$",c:[{cN:"string",b:" ",e:" ",eB:true,eE:true}]},{cN:"attribute",b:"^\\w",e:": ",eE:true,i:"\\n|\\s|=",starts:{cN:"string",e:"$"}},{b:"\\n\\n",starts:{sL:"",eW:true}}]}});hljs.registerLanguage("ini",function(a){return{cI:true,i:/\S/,c:[{cN:"comment",b:";",e:"$"},{cN:"title",b:"^\\[",e:"\\]"},{cN:"setting",b:"^[a-z0-9\\[\\]_-]+[ \\t]*=[ \\t]*",e:"$",c:[{cN:"value",eW:true,k:"on off true false yes no",c:[a.QSM,a.NM],r:0}]}]}});hljs.registerLanguage("java",function(c){var b=c.UIR+"(<"+c.UIR+">)?";var a="false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private";return{aliases:["jsp"],k:a,i:/<\//,c:[{cN:"javadoc",b:"/\\*\\*",e:"\\*/",r:0,c:[{cN:"javadoctag",b:"(^|\\s)@[A-Za-z]+"}]},c.CLCM,c.CBCM,c.ASM,c.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:true,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},c.UTM]},{bK:"new",e:/\s/,r:0},{cN:"function",b:"("+b+"\\s+)+"+c.UIR+"\\s*\\(",rB:true,e:/[{;=]/,eE:true,k:a,c:[{b:c.UIR+"\\s*\\(",rB:true,c:[c.UTM]},{cN:"params",b:/\(/,e:/\)/,k:a,c:[c.ASM,c.QSM,c.CNM,c.CBCM]},c.CLCM,c.CBCM]},c.CNM,{cN:"annotation",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("javascript",function(a){return{aliases:["js"],k:{keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document"},c:[{cN:"pi",b:/^\s*('|")use strict('|")/,r:10},a.ASM,a.QSM,a.CLCM,a.CBCM,a.CNM,{b:"("+a.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[a.CLCM,a.CBCM,a.RM,{b:/</,e:/>;/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:true,c:[a.inherit(a.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,c:[a.CLCM,a.CBCM],i:/["'\(]/}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+a.IR,r:0}]}});hljs.registerLanguage("json",function(a){var e={literal:"true false null"};var d=[a.QSM,a.CNM];var c={cN:"value",e:",",eW:true,eE:true,c:d,k:e};var b={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:true,eE:true,c:[a.BE],i:"\\n",starts:c}],i:"\\S"};var f={b:"\\[",e:"\\]",c:[a.inherit(c,{cN:null})],i:"\\S"};d.splice(d.length,0,b,f);return{c:d,k:e,i:"\\S"}});hljs.registerLanguage("lasso",function(d){var b="[a-zA-Z_][a-zA-Z0-9_.]*";var i="<\\?(lasso(script)?|=)";var c="\\]|\\?>";var g={literal:"true false none minimal full all void and or not bw nbw ew new cn ncn lt lte gt gte eq neq rx nrx ft",built_in:"array date decimal duration integer map pair string tag xml null bytes list queue set stack staticarray tie local var variable global data self inherited",keyword:"error_code error_msg error_pop error_push error_reset cache database_names database_schemanames database_tablenames define_tag define_type email_batch encode_set html_comment handle handle_error header if inline iterate ljax_target link link_currentaction link_currentgroup link_currentrecord link_detail link_firstgroup link_firstrecord link_lastgroup link_lastrecord link_nextgroup link_nextrecord link_prevgroup link_prevrecord log loop namespace_using output_none portal private protect records referer referrer repeating resultset rows search_args search_arguments select sort_args sort_arguments thread_atomic value_list while abort case else if_empty if_false if_null if_true loop_abort loop_continue loop_count params params_up return return_value run_children soap_definetag soap_lastrequest soap_lastresponse tag_name ascending average by define descending do equals frozen group handle_failure import in into join let match max min on order parent protected provide public require returnhome skip split_thread sum take thread to trait type where with yield yieldhome"};var a={cN:"comment",b:"<!--",e:"-->",r:0};var j={cN:"preprocessor",b:"\\[noprocess\\]",starts:{cN:"markup",e:"\\[/noprocess\\]",rE:true,c:[a]}};var e={cN:"preprocessor",b:"\\[/noprocess|"+i};var h={cN:"variable",b:"'"+b+"'"};var f=[d.CLCM,{cN:"javadoc",b:"/\\*\\*!",e:"\\*/",c:[d.PWM]},d.CBCM,d.inherit(d.CNM,{b:d.CNR+"|-?(infinity|nan)\\b"}),d.inherit(d.ASM,{i:null}),d.inherit(d.QSM,{i:null}),{cN:"string",b:"`",e:"`"},{cN:"variable",v:[{b:"[#$]"+b},{b:"#",e:"\\d+",i:"\\W"}]},{cN:"tag",b:"::\\s*",e:b,i:"\\W"},{cN:"attribute",v:[{b:"-"+d.UIR,r:0},{b:"(\\.\\.\\.)"}]},{cN:"subst",v:[{b:"->\\s*",c:[h]},{b:":=|/(?!\\w)=?|[-+*%=<>&|!?\\\\]+",r:0}]},{cN:"built_in",b:"\\.\\.?",r:0,c:[h]},{cN:"class",bK:"define",rE:true,e:"\\(|=>",c:[d.inherit(d.TM,{b:d.UIR+"(=(?!>))?"})]}];return{aliases:["ls","lassoscript"],cI:true,l:b+"|&[lg]t;",k:g,c:[{cN:"preprocessor",b:c,r:0,starts:{cN:"markup",e:"\\[|"+i,rE:true,r:0,c:[a]}},j,e,{cN:"preprocessor",b:"\\[no_square_brackets",starts:{e:"\\[/no_square_brackets\\]",l:b+"|&[lg]t;",k:g,c:[{cN:"preprocessor",b:c,r:0,starts:{cN:"markup",e:i,rE:true,c:[a]}},j,e].concat(f)}},{cN:"preprocessor",b:"\\[",r:0},{cN:"shebang",b:"^#!.+lasso9\\b",r:10}].concat(f)}});hljs.registerLanguage("lisp",function(i){var l="[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*";var m="(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s)(\\+|\\-)?\\d+)?";var k={cN:"shebang",b:"^#!",e:"$"};var b={cN:"literal",b:"\\b(t{1}|nil)\\b"};var e={cN:"number",v:[{b:m,r:0},{b:"#b[0-1]+(/[0-1]+)?"},{b:"#o[0-7]+(/[0-7]+)?"},{b:"#x[0-9a-f]+(/[0-9a-f]+)?"},{b:"#c\\("+m+" +"+m,e:"\\)"}]};var h=i.inherit(i.QSM,{i:null});var n={cN:"comment",b:";",e:"$",r:0};var g={cN:"variable",b:"\\*",e:"\\*"};var o={cN:"keyword",b:"[:&]"+l};var d={b:"\\(",e:"\\)",c:["self",b,h,e]};var a={cN:"quoted",c:[e,h,g,o,d],v:[{b:"['`]\\(",e:"\\)"},{b:"\\(quote ",e:"\\)",k:"quote"}]};var c={cN:"quoted",b:"'"+l};var j={cN:"list",b:"\\(",e:"\\)"};var f={eW:true,r:0};j.c=[{cN:"keyword",b:l},f];f.c=[a,c,j,b,e,h,n,g,o];return{i:/\S/,c:[e,k,b,h,n,a,c,j]}});hljs.registerLanguage("livecodeserver",function(a){var e={cN:"variable",b:"\\b[gtps][A-Z]+[A-Za-z0-9_\\-]*\\b|\\$_[A-Z]+",r:0};var b={cN:"comment",e:"$",v:[a.CBCM,a.HCM,{b:"--"},{b:"[^:]//"}]};var d=a.inherit(a.TM,{v:[{b:"\\b_*rig[A-Z]+[A-Za-z0-9_\\-]*"},{b:"\\b_[a-z0-9\\-]+"}]});var c=a.inherit(a.TM,{b:"\\b([A-Za-z0-9_\\-]+)\\b"});return{cI:false,k:{keyword:"after byte bytes english the until http forever descending using line real8 with seventh for stdout finally element word fourth before black ninth sixth characters chars stderr uInt1 uInt1s uInt2 uInt2s stdin string lines relative rel any fifth items from middle mid at else of catch then third it file milliseconds seconds second secs sec int1 int1s int4 int4s internet int2 int2s normal text item last long detailed effective uInt4 uInt4s repeat end repeat URL in try into switch to words https token binfile each tenth as ticks tick system real4 by dateItems without char character ascending eighth whole dateTime numeric short first ftp integer abbreviated abbr abbrev private case while if",constant:"SIX TEN FORMFEED NINE ZERO NONE SPACE FOUR FALSE COLON CRLF PI COMMA ENDOFFILE EOF EIGHT FIVE QUOTE EMPTY ONE TRUE RETURN CR LINEFEED RIGHT BACKSLASH NULL SEVEN TAB THREE TWO six ten formfeed nine zero none space four false colon crlf pi comma endoffile eof eight five quote empty one true return cr linefeed right backslash null seven tab three two RIVERSION RISTATE FILE_READ_MODE FILE_WRITE_MODE FILE_WRITE_MODE DIR_WRITE_MODE FILE_READ_UMASK FILE_WRITE_UMASK DIR_READ_UMASK DIR_WRITE_UMASK",operator:"div mod wrap and or bitAnd bitNot bitOr bitXor among not in a an within contains ends with begins the keys of keys",built_in:"put abs acos aliasReference annuity arrayDecode arrayEncode asin atan atan2 average avg base64Decode base64Encode baseConvert binaryDecode binaryEncode byteToNum cachedURL cachedURLs charToNum cipherNames commandNames compound compress constantNames cos date dateFormat decompress directories diskSpace DNSServers exp exp1 exp2 exp10 extents files flushEvents folders format functionNames global globals hasMemory hostAddress hostAddressToName hostName hostNameToAddress isNumber ISOToMac itemOffset keys len length libURLErrorData libUrlFormData libURLftpCommand libURLLastHTTPHeaders libURLLastRHHeaders libUrlMultipartFormAddPart libUrlMultipartFormData libURLVersion lineOffset ln ln1 localNames log log2 log10 longFilePath lower macToISO matchChunk matchText matrixMultiply max md5Digest median merge millisec millisecs millisecond milliseconds min monthNames num number numToByte numToChar offset open openfiles openProcesses openProcessIDs openSockets paramCount param params peerAddress pendingMessages platform processID random randomBytes replaceText result revCreateXMLTree revCreateXMLTreeFromFile revCurrentRecord revCurrentRecordIsFirst revCurrentRecordIsLast revDatabaseColumnCount revDatabaseColumnIsNull revDatabaseColumnLengths revDatabaseColumnNames revDatabaseColumnNamed revDatabaseColumnNumbered revDatabaseColumnTypes revDatabaseConnectResult revDatabaseCursors revDatabaseID revDatabaseTableNames revDatabaseType revDataFromQuery revdb_closeCursor revdb_columnbynumber revdb_columncount revdb_columnisnull revdb_columnlengths revdb_columnnames revdb_columntypes revdb_commit revdb_connect revdb_connections revdb_connectionerr revdb_currentrecord revdb_cursorconnection revdb_cursorerr revdb_cursors revdb_dbtype revdb_disconnect revdb_execute revdb_iseof revdb_isbof revdb_movefirst revdb_movelast revdb_movenext revdb_moveprev revdb_query revdb_querylist revdb_recordcount revdb_rollback revdb_tablenames revGetDatabaseDriverPath revNumberOfRecords revOpenDatabase revOpenDatabases revQueryDatabase revQueryDatabaseBlob revQueryResult revQueryIsAtStart revQueryIsAtEnd revUnixFromMacPath revXMLAttribute revXMLAttributes revXMLAttributeValues revXMLChildContents revXMLChildNames revXMLFirstChild revXMLMatchingNode revXMLNextSibling revXMLNodeContents revXMLNumberOfChildren revXMLParent revXMLPreviousSibling revXMLRootNode revXMLRPC_CreateRequest revXMLRPC_Documents revXMLRPC_Error revXMLRPC_Execute revXMLRPC_GetHost revXMLRPC_GetMethod revXMLRPC_GetParam revXMLText revXMLRPC_GetParamCount revXMLRPC_GetParamNode revXMLRPC_GetParamType revXMLRPC_GetPath revXMLRPC_GetPort revXMLRPC_GetProtocol revXMLRPC_GetRequest revXMLRPC_GetResponse revXMLRPC_GetSocket revXMLTree revXMLTrees revXMLValidateDTD revZipDescribeItem revZipEnumerateItems revZipOpenArchives round sec secs seconds sha1Digest shell shortFilePath sin specialFolderPath sqrt standardDeviation statRound stdDev sum sysError systemVersion tan tempName tick ticks time to toLower toUpper transpose trunc uniDecode uniEncode upper URLDecode URLEncode URLStatus value variableNames version waitDepth weekdayNames wordOffset add breakpoint cancel clear local variable file word line folder directory URL close socket process combine constant convert create new alias folder directory decrypt delete variable word line folder directory URL dispatch divide do encrypt filter get include intersect kill libURLDownloadToFile libURLFollowHttpRedirects libURLftpUpload libURLftpUploadFile libURLresetAll libUrlSetAuthCallback libURLSetCustomHTTPHeaders libUrlSetExpect100 libURLSetFTPListCommand libURLSetFTPMode libURLSetFTPStopTime libURLSetStatusCallback load multiply socket process post seek rel relative read from process rename replace require resetAll revAddXMLNode revAppendXML revCloseCursor revCloseDatabase revCommitDatabase revCopyFile revCopyFolder revCopyXMLNode revDeleteFolder revDeleteXMLNode revDeleteAllXMLTrees revDeleteXMLTree revExecuteSQL revGoURL revInsertXMLNode revMoveFolder revMoveToFirstRecord revMoveToLastRecord revMoveToNextRecord revMoveToPreviousRecord revMoveToRecord revMoveXMLNode revPutIntoXMLNode revRollBackDatabase revSetDatabaseDriverPath revSetXMLAttribute revXMLRPC_AddParam revXMLRPC_DeleteAllDocuments revXMLAddDTD revXMLRPC_Free revXMLRPC_FreeAll revXMLRPC_DeleteDocument revXMLRPC_DeleteParam revXMLRPC_SetHost revXMLRPC_SetMethod revXMLRPC_SetPort revXMLRPC_SetProtocol revXMLRPC_SetSocket revZipAddItemWithData revZipAddItemWithFile revZipAddUncompressedItemWithData revZipAddUncompressedItemWithFile revZipCancel revZipCloseArchive revZipDeleteItem revZipExtractItemToFile revZipExtractItemToVariable revZipSetProgressCallback revZipRenameItem revZipReplaceItemWithData revZipReplaceItemWithFile revZipOpenArchive send set sort split subtract union unload wait write"},c:[e,{cN:"keyword",b:"\\bend\\sif\\b"},{cN:"function",bK:"function",e:"$",c:[e,c,a.ASM,a.QSM,a.BNM,a.CNM,d]},{cN:"function",bK:"end",e:"$",c:[c,d]},{cN:"command",bK:"command on",e:"$",c:[e,c,a.ASM,a.QSM,a.BNM,a.CNM,d]},{cN:"command",bK:"end",e:"$",c:[c,d]},{cN:"preprocessor",b:"<\\?rev|<\\?lc|<\\?livecode",r:10},{cN:"preprocessor",b:"<\\?"},{cN:"preprocessor",b:"\\?>"},b,a.ASM,a.QSM,a.BNM,a.CNM,d],i:";$|^\\[|^="}});hljs.registerLanguage("lua",function(b){var a="\\[=*\\[";var e="\\]=*\\]";var c={b:a,e:e,c:["self"]};var d=[{cN:"comment",b:"--(?!"+a+")",e:"$"},{cN:"comment",b:"--"+a,e:e,c:[c],r:10}];return{l:b.UIR,k:{keyword:"and break do else elseif end false for if in local nil not or repeat return then true until while",built_in:"_G _VERSION assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall coroutine debug io math os package string table"},c:d.concat([{cN:"function",bK:"function",e:"\\)",c:[b.inherit(b.TM,{b:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{cN:"params",b:"\\(",eW:true,c:d}].concat(d)},b.CNM,b.ASM,b.QSM,{cN:"string",b:a,e:e,c:[c],r:5}])}});hljs.registerLanguage("makefile",function(a){var b={cN:"variable",b:/\$\(/,e:/\)/,c:[a.BE]};return{aliases:["mk","mak"],c:[a.HCM,{b:/^\w+\s*\W*=/,rB:true,r:0,starts:{cN:"constant",e:/\s*\W*=/,eE:true,starts:{e:/$/,r:0,c:[b]}}},{cN:"title",b:/^[\w]+:\s*$/},{cN:"phony",b:/^\.PHONY:/,e:/$/,k:".PHONY",l:/[\.\w]+/},{b:/^\t+/,e:/$/,r:0,c:[a.QSM,b]}]}});hljs.registerLanguage("mathematica",function(a){return{aliases:["mma"],l:"(\\$|\\b)"+a.IR+"\\b",k:"AbelianGroup Abort AbortKernels AbortProtect Above Abs Absolute AbsoluteCorrelation AbsoluteCorrelationFunction AbsoluteCurrentValue AbsoluteDashing AbsoluteFileName AbsoluteOptions AbsolutePointSize AbsoluteThickness AbsoluteTime AbsoluteTiming AccountingForm Accumulate Accuracy AccuracyGoal ActionDelay ActionMenu ActionMenuBox ActionMenuBoxOptions Active ActiveItem ActiveStyle AcyclicGraphQ AddOnHelpPath AddTo AdjacencyGraph AdjacencyList AdjacencyMatrix AdjustmentBox AdjustmentBoxOptions AdjustTimeSeriesForecast AffineTransform After AiryAi AiryAiPrime AiryAiZero AiryBi AiryBiPrime AiryBiZero AlgebraicIntegerQ AlgebraicNumber AlgebraicNumberDenominator AlgebraicNumberNorm AlgebraicNumberPolynomial AlgebraicNumberTrace AlgebraicRules AlgebraicRulesData Algebraics AlgebraicUnitQ Alignment AlignmentMarker AlignmentPoint All AllowedDimensions AllowGroupClose AllowInlineCells AllowKernelInitialization AllowReverseGroupClose AllowScriptLevelChange AlphaChannel AlternatingGroup AlternativeHypothesis Alternatives AmbientLight Analytic AnchoredSearch And AndersonDarlingTest AngerJ AngleBracket AngularGauge Animate AnimationCycleOffset AnimationCycleRepetitions AnimationDirection AnimationDisplayTime AnimationRate AnimationRepetitions AnimationRunning Animator AnimatorBox AnimatorBoxOptions AnimatorElements Annotation Annuity AnnuityDue Antialiasing Antisymmetric Apart ApartSquareFree Appearance AppearanceElements AppellF1 Append AppendTo Apply ArcCos ArcCosh ArcCot ArcCoth ArcCsc ArcCsch ArcSec ArcSech ArcSin ArcSinDistribution ArcSinh ArcTan ArcTanh Arg ArgMax ArgMin ArgumentCountQ ARIMAProcess ArithmeticGeometricMean ARMAProcess ARProcess Array ArrayComponents ArrayDepth ArrayFlatten ArrayPad ArrayPlot ArrayQ ArrayReshape ArrayRules Arrays Arrow Arrow3DBox ArrowBox Arrowheads AspectRatio AspectRatioFixed Assert Assuming Assumptions AstronomicalData Asynchronous AsynchronousTaskObject AsynchronousTasks AtomQ Attributes AugmentedSymmetricPolynomial AutoAction AutoDelete AutoEvaluateEvents AutoGeneratedPackage AutoIndent AutoIndentSpacings AutoItalicWords AutoloadPath AutoMatch Automatic AutomaticImageSize AutoMultiplicationSymbol AutoNumberFormatting AutoOpenNotebooks AutoOpenPalettes AutorunSequencing AutoScaling AutoScroll AutoSpacing AutoStyleOptions AutoStyleWords Axes AxesEdge AxesLabel AxesOrigin AxesStyle Axis BabyMonsterGroupB Back Background BackgroundTasksSettings Backslash Backsubstitution Backward Band BandpassFilter BandstopFilter BarabasiAlbertGraphDistribution BarChart BarChart3D BarLegend BarlowProschanImportance BarnesG BarOrigin BarSpacing BartlettHannWindow BartlettWindow BaseForm Baseline BaselinePosition BaseStyle BatesDistribution BattleLemarieWavelet Because BeckmannDistribution Beep Before Begin BeginDialogPacket BeginFrontEndInteractionPacket BeginPackage BellB BellY Below BenfordDistribution BeniniDistribution BenktanderGibratDistribution BenktanderWeibullDistribution BernoulliB BernoulliDistribution BernoulliGraphDistribution BernoulliProcess BernsteinBasis BesselFilterModel BesselI BesselJ BesselJZero BesselK BesselY BesselYZero Beta BetaBinomialDistribution BetaDistribution BetaNegativeBinomialDistribution BetaPrimeDistribution BetaRegularized BetweennessCentrality BezierCurve BezierCurve3DBox BezierCurve3DBoxOptions BezierCurveBox BezierCurveBoxOptions BezierFunction BilateralFilter Binarize BinaryFormat BinaryImageQ BinaryRead BinaryReadList BinaryWrite BinCounts BinLists Binomial BinomialDistribution BinomialProcess BinormalDistribution BiorthogonalSplineWavelet BipartiteGraphQ BirnbaumImportance BirnbaumSaundersDistribution BitAnd BitClear BitGet BitLength BitNot BitOr BitSet BitShiftLeft BitShiftRight BitXor Black BlackmanHarrisWindow BlackmanNuttallWindow BlackmanWindow Blank BlankForm BlankNullSequence BlankSequence Blend Block BlockRandom BlomqvistBeta BlomqvistBetaTest Blue Blur BodePlot BohmanWindow Bold Bookmarks Boole BooleanConsecutiveFunction BooleanConvert BooleanCountingFunction BooleanFunction BooleanGraph BooleanMaxterms BooleanMinimize BooleanMinterms Booleans BooleanTable BooleanVariables BorderDimensions BorelTannerDistribution Bottom BottomHatTransform BoundaryStyle Bounds Box BoxBaselineShift BoxData BoxDimensions Boxed Boxes BoxForm BoxFormFormatTypes BoxFrame BoxID BoxMargins BoxMatrix BoxRatios BoxRotation BoxRotationPoint BoxStyle BoxWhiskerChart Bra BracketingBar BraKet BrayCurtisDistance BreadthFirstScan Break Brown BrownForsytheTest BrownianBridgeProcess BrowserCategory BSplineBasis BSplineCurve BSplineCurve3DBox BSplineCurveBox BSplineCurveBoxOptions BSplineFunction BSplineSurface BSplineSurface3DBox BubbleChart BubbleChart3D BubbleScale BubbleSizes BulletGauge BusinessDayQ ButterflyGraph ButterworthFilterModel Button ButtonBar ButtonBox ButtonBoxOptions ButtonCell ButtonContents ButtonData ButtonEvaluator ButtonExpandable ButtonFrame ButtonFunction ButtonMargins ButtonMinHeight ButtonNote ButtonNotebook ButtonSource ButtonStyle ButtonStyleMenuListing Byte ByteCount ByteOrdering C CachedValue CacheGraphics CalendarData CalendarType CallPacket CanberraDistance Cancel CancelButton CandlestickChart Cap CapForm CapitalDifferentialD CardinalBSplineBasis CarmichaelLambda Cases Cashflow Casoratian Catalan CatalanNumber Catch CauchyDistribution CauchyWindow CayleyGraph CDF CDFDeploy CDFInformation CDFWavelet Ceiling Cell CellAutoOverwrite CellBaseline CellBoundingBox CellBracketOptions CellChangeTimes CellContents CellContext CellDingbat CellDynamicExpression CellEditDuplicate CellElementsBoundingBox CellElementSpacings CellEpilog CellEvaluationDuplicate CellEvaluationFunction CellEventActions CellFrame CellFrameColor CellFrameLabelMargins CellFrameLabels CellFrameMargins CellGroup CellGroupData CellGrouping CellGroupingRules CellHorizontalScrolling CellID CellLabel CellLabelAutoDelete CellLabelMargins CellLabelPositioning CellMargins CellObject CellOpen CellPrint CellProlog Cells CellSize CellStyle CellTags CellularAutomaton CensoredDistribution Censoring Center CenterDot CentralMoment CentralMomentGeneratingFunction CForm ChampernowneNumber ChanVeseBinarize Character CharacterEncoding CharacterEncodingsPath CharacteristicFunction CharacteristicPolynomial CharacterRange Characters ChartBaseStyle ChartElementData ChartElementDataFunction ChartElementFunction ChartElements ChartLabels ChartLayout ChartLegends ChartStyle Chebyshev1FilterModel Chebyshev2FilterModel ChebyshevDistance ChebyshevT ChebyshevU Check CheckAbort CheckAll Checkbox CheckboxBar CheckboxBox CheckboxBoxOptions ChemicalData ChessboardDistance ChiDistribution ChineseRemainder ChiSquareDistribution ChoiceButtons ChoiceDialog CholeskyDecomposition Chop Circle CircleBox CircleDot CircleMinus CirclePlus CircleTimes CirculantGraph CityData Clear ClearAll ClearAttributes ClearSystemCache ClebschGordan ClickPane Clip ClipboardNotebook ClipFill ClippingStyle ClipPlanes ClipRange Clock ClockGauge ClockwiseContourIntegral Close Closed CloseKernels ClosenessCentrality Closing ClosingAutoSave ClosingEvent ClusteringComponents CMYKColor Coarse Coefficient CoefficientArrays CoefficientDomain CoefficientList CoefficientRules CoifletWavelet Collect Colon ColonForm ColorCombine ColorConvert ColorData ColorDataFunction ColorFunction ColorFunctionScaling Colorize ColorNegate ColorOutput ColorProfileData ColorQuantize ColorReplace ColorRules ColorSelectorSettings ColorSeparate ColorSetter ColorSetterBox ColorSetterBoxOptions ColorSlider ColorSpace Column ColumnAlignments ColumnBackgrounds ColumnForm ColumnLines ColumnsEqual ColumnSpacings ColumnWidths CommonDefaultFormatTypes Commonest CommonestFilter CommonUnits CommunityBoundaryStyle CommunityGraphPlot CommunityLabels CommunityRegionStyle CompatibleUnitQ CompilationOptions CompilationTarget Compile Compiled CompiledFunction Complement CompleteGraph CompleteGraphQ CompleteKaryTree CompletionsListPacket Complex Complexes ComplexExpand ComplexInfinity ComplexityFunction ComponentMeasurements ComponentwiseContextMenu Compose ComposeList ComposeSeries Composition CompoundExpression CompoundPoissonDistribution CompoundPoissonProcess CompoundRenewalProcess Compress CompressedData Condition ConditionalExpression Conditioned Cone ConeBox ConfidenceLevel ConfidenceRange ConfidenceTransform ConfigurationPath Congruent Conjugate ConjugateTranspose Conjunction Connect ConnectedComponents ConnectedGraphQ ConnesWindow ConoverTest ConsoleMessage ConsoleMessagePacket ConsolePrint Constant ConstantArray Constants ConstrainedMax ConstrainedMin ContentPadding ContentsBoundingBox ContentSelectable ContentSize Context ContextMenu Contexts ContextToFilename ContextToFileName Continuation Continue ContinuedFraction ContinuedFractionK ContinuousAction ContinuousMarkovProcess ContinuousTimeModelQ ContinuousWaveletData ContinuousWaveletTransform ContourDetect ContourGraphics ContourIntegral ContourLabels ContourLines ContourPlot ContourPlot3D Contours ContourShading ContourSmoothing ContourStyle ContraharmonicMean Control ControlActive ControlAlignment ControllabilityGramian ControllabilityMatrix ControllableDecomposition ControllableModelQ ControllerDuration ControllerInformation ControllerInformationData ControllerLinking ControllerManipulate ControllerMethod ControllerPath ControllerState ControlPlacement ControlsRendering ControlType Convergents ConversionOptions ConversionRules ConvertToBitmapPacket ConvertToPostScript ConvertToPostScriptPacket Convolve ConwayGroupCo1 ConwayGroupCo2 ConwayGroupCo3 CoordinateChartData CoordinatesToolOptions CoordinateTransform CoordinateTransformData CoprimeQ Coproduct CopulaDistribution Copyable CopyDirectory CopyFile CopyTag CopyToClipboard CornerFilter CornerNeighbors Correlation CorrelationDistance CorrelationFunction CorrelationTest Cos Cosh CoshIntegral CosineDistance CosineWindow CosIntegral Cot Coth Count CounterAssignments CounterBox CounterBoxOptions CounterClockwiseContourIntegral CounterEvaluator CounterFunction CounterIncrements CounterStyle CounterStyleMenuListing CountRoots CountryData Covariance CovarianceEstimatorFunction CovarianceFunction CoxianDistribution CoxIngersollRossProcess CoxModel CoxModelFit CramerVonMisesTest CreateArchive CreateDialog CreateDirectory CreateDocument CreateIntermediateDirectories CreatePalette CreatePalettePacket CreateScheduledTask CreateTemporary CreateWindow CriticalityFailureImportance CriticalitySuccessImportance CriticalSection Cross CrossingDetect CrossMatrix Csc Csch CubeRoot Cubics Cuboid CuboidBox Cumulant CumulantGeneratingFunction Cup CupCap Curl CurlyDoubleQuote CurlyQuote CurrentImage CurrentlySpeakingPacket CurrentValue CurvatureFlowFilter CurveClosed Cyan CycleGraph CycleIndexPolynomial Cycles CyclicGroup Cyclotomic Cylinder CylinderBox CylindricalDecomposition D DagumDistribution DamerauLevenshteinDistance DampingFactor Darker Dashed Dashing DataCompression DataDistribution DataRange DataReversed Date DateDelimiters DateDifference DateFunction DateList DateListLogPlot DateListPlot DatePattern DatePlus DateRange DateString DateTicksFormat DaubechiesWavelet DavisDistribution DawsonF DayCount DayCountConvention DayMatchQ DayName DayPlus DayRange DayRound DeBruijnGraph Debug DebugTag Decimal DeclareKnownSymbols DeclarePackage Decompose Decrement DedekindEta Default DefaultAxesStyle DefaultBaseStyle DefaultBoxStyle DefaultButton DefaultColor DefaultControlPlacement DefaultDuplicateCellStyle DefaultDuration DefaultElement DefaultFaceGridsStyle DefaultFieldHintStyle DefaultFont DefaultFontProperties DefaultFormatType DefaultFormatTypeForStyle DefaultFrameStyle DefaultFrameTicksStyle DefaultGridLinesStyle DefaultInlineFormatType DefaultInputFormatType DefaultLabelStyle DefaultMenuStyle DefaultNaturalLanguage DefaultNewCellStyle DefaultNewInlineCellStyle DefaultNotebook DefaultOptions DefaultOutputFormatType DefaultStyle DefaultStyleDefinitions DefaultTextFormatType DefaultTextInlineFormatType DefaultTicksStyle DefaultTooltipStyle DefaultValues Defer DefineExternal DefineInputStreamMethod DefineOutputStreamMethod Definition Degree DegreeCentrality DegreeGraphDistribution DegreeLexicographic DegreeReverseLexicographic Deinitialization Del Deletable Delete DeleteBorderComponents DeleteCases DeleteContents DeleteDirectory DeleteDuplicates DeleteFile DeleteSmallComponents DeleteWithContents DeletionWarning Delimiter DelimiterFlashTime DelimiterMatching Delimiters Denominator DensityGraphics DensityHistogram DensityPlot DependentVariables Deploy Deployed Depth DepthFirstScan Derivative DerivativeFilter DescriptorStateSpace DesignMatrix Det DGaussianWavelet DiacriticalPositioning Diagonal DiagonalMatrix Dialog DialogIndent DialogInput DialogLevel DialogNotebook DialogProlog DialogReturn DialogSymbols Diamond DiamondMatrix DiceDissimilarity DictionaryLookup DifferenceDelta DifferenceOrder DifferenceRoot DifferenceRootReduce Differences DifferentialD DifferentialRoot DifferentialRootReduce DifferentiatorFilter DigitBlock DigitBlockMinimum DigitCharacter DigitCount DigitQ DihedralGroup Dilation Dimensions DiracComb DiracDelta DirectedEdge DirectedEdges DirectedGraph DirectedGraphQ DirectedInfinity Direction Directive Directory DirectoryName DirectoryQ DirectoryStack DirichletCharacter DirichletConvolve DirichletDistribution DirichletL DirichletTransform DirichletWindow DisableConsolePrintPacket DiscreteChirpZTransform DiscreteConvolve DiscreteDelta DiscreteHadamardTransform DiscreteIndicator DiscreteLQEstimatorGains DiscreteLQRegulatorGains DiscreteLyapunovSolve DiscreteMarkovProcess DiscretePlot DiscretePlot3D DiscreteRatio DiscreteRiccatiSolve DiscreteShift DiscreteTimeModelQ DiscreteUniformDistribution DiscreteVariables DiscreteWaveletData DiscreteWaveletPacketTransform DiscreteWaveletTransform Discriminant Disjunction Disk DiskBox DiskMatrix Dispatch DispersionEstimatorFunction Display DisplayAllSteps DisplayEndPacket DisplayFlushImagePacket DisplayForm DisplayFunction DisplayPacket DisplayRules DisplaySetSizePacket DisplayString DisplayTemporary DisplayWith DisplayWithRef DisplayWithVariable DistanceFunction DistanceTransform Distribute Distributed DistributedContexts DistributeDefinitions DistributionChart DistributionDomain DistributionFitTest DistributionParameterAssumptions DistributionParameterQ Dithering Div Divergence Divide DivideBy Dividers Divisible Divisors DivisorSigma DivisorSum DMSList DMSString Do DockedCells DocumentNotebook DominantColors DOSTextFormat Dot DotDashed DotEqual Dotted DoubleBracketingBar DoubleContourIntegral DoubleDownArrow DoubleLeftArrow DoubleLeftRightArrow DoubleLeftTee DoubleLongLeftArrow DoubleLongLeftRightArrow DoubleLongRightArrow DoubleRightArrow DoubleRightTee DoubleUpArrow DoubleUpDownArrow DoubleVerticalBar DoublyInfinite Down DownArrow DownArrowBar DownArrowUpArrow DownLeftRightVector DownLeftTeeVector DownLeftVector DownLeftVectorBar DownRightTeeVector DownRightVector DownRightVectorBar Downsample DownTee DownTeeArrow DownValues DragAndDrop DrawEdges DrawFrontFaces DrawHighlighted Drop DSolve Dt DualLinearProgramming DualSystemsModel DumpGet DumpSave DuplicateFreeQ Dynamic DynamicBox DynamicBoxOptions DynamicEvaluationTimeout DynamicLocation DynamicModule DynamicModuleBox DynamicModuleBoxOptions DynamicModuleParent DynamicModuleValues DynamicName DynamicNamespace DynamicReference DynamicSetting DynamicUpdating DynamicWrapper DynamicWrapperBox DynamicWrapperBoxOptions E EccentricityCentrality EdgeAdd EdgeBetweennessCentrality EdgeCapacity EdgeCapForm EdgeColor EdgeConnectivity EdgeCost EdgeCount EdgeCoverQ EdgeDashing EdgeDelete EdgeDetect EdgeForm EdgeIndex EdgeJoinForm EdgeLabeling EdgeLabels EdgeLabelStyle EdgeList EdgeOpacity EdgeQ EdgeRenderingFunction EdgeRules EdgeShapeFunction EdgeStyle EdgeThickness EdgeWeight Editable EditButtonSettings EditCellTagsSettings EditDistance EffectiveInterest Eigensystem Eigenvalues EigenvectorCentrality Eigenvectors Element ElementData Eliminate EliminationOrder EllipticE EllipticExp EllipticExpPrime EllipticF EllipticFilterModel EllipticK EllipticLog EllipticNomeQ EllipticPi EllipticReducedHalfPeriods EllipticTheta EllipticThetaPrime EmitSound EmphasizeSyntaxErrors EmpiricalDistribution Empty EmptyGraphQ EnableConsolePrintPacket Enabled Encode End EndAdd EndDialogPacket EndFrontEndInteractionPacket EndOfFile EndOfLine EndOfString EndPackage EngineeringForm Enter EnterExpressionPacket EnterTextPacket Entropy EntropyFilter Environment Epilog Equal EqualColumns EqualRows EqualTilde EquatedTo Equilibrium EquirippleFilterKernel Equivalent Erf Erfc Erfi ErlangB ErlangC ErlangDistribution Erosion ErrorBox ErrorBoxOptions ErrorNorm ErrorPacket ErrorsDialogSettings EstimatedDistribution EstimatedProcess EstimatorGains EstimatorRegulator EuclideanDistance EulerE EulerGamma EulerianGraphQ EulerPhi Evaluatable Evaluate Evaluated EvaluatePacket EvaluationCell EvaluationCompletionAction EvaluationElements EvaluationMode EvaluationMonitor EvaluationNotebook EvaluationObject EvaluationOrder Evaluator EvaluatorNames EvenQ EventData EventEvaluator EventHandler EventHandlerTag EventLabels ExactBlackmanWindow ExactNumberQ ExactRootIsolation ExampleData Except ExcludedForms ExcludePods Exclusions ExclusionsStyle Exists Exit ExitDialog Exp Expand ExpandAll ExpandDenominator ExpandFileName ExpandNumerator Expectation ExpectationE ExpectedValue ExpGammaDistribution ExpIntegralE ExpIntegralEi Exponent ExponentFunction ExponentialDistribution ExponentialFamily ExponentialGeneratingFunction ExponentialMovingAverage ExponentialPowerDistribution ExponentPosition ExponentStep Export ExportAutoReplacements ExportPacket ExportString Expression ExpressionCell ExpressionPacket ExpToTrig ExtendedGCD Extension ExtentElementFunction ExtentMarkers ExtentSize ExternalCall ExternalDataCharacterEncoding Extract ExtractArchive ExtremeValueDistribution FaceForm FaceGrids FaceGridsStyle Factor FactorComplete Factorial Factorial2 FactorialMoment FactorialMomentGeneratingFunction FactorialPower FactorInteger FactorList FactorSquareFree FactorSquareFreeList FactorTerms FactorTermsList Fail FailureDistribution False FARIMAProcess FEDisableConsolePrintPacket FeedbackSector FeedbackSectorStyle FeedbackType FEEnableConsolePrintPacket Fibonacci FieldHint FieldHintStyle FieldMasked FieldSize File FileBaseName FileByteCount FileDate FileExistsQ FileExtension FileFormat FileHash FileInformation FileName FileNameDepth FileNameDialogSettings FileNameDrop FileNameJoin FileNames FileNameSetter FileNameSplit FileNameTake FilePrint FileType FilledCurve FilledCurveBox Filling FillingStyle FillingTransform FilterRules FinancialBond FinancialData FinancialDerivative FinancialIndicator Find FindArgMax FindArgMin FindClique FindClusters FindCurvePath FindDistributionParameters FindDivisions FindEdgeCover FindEdgeCut FindEulerianCycle FindFaces FindFile FindFit FindGeneratingFunction FindGeoLocation FindGeometricTransform FindGraphCommunities FindGraphIsomorphism FindGraphPartition FindHamiltonianCycle FindIndependentEdgeSet FindIndependentVertexSet FindInstance FindIntegerNullVector FindKClan FindKClique FindKClub FindKPlex FindLibrary FindLinearRecurrence FindList FindMaximum FindMaximumFlow FindMaxValue FindMinimum FindMinimumCostFlow FindMinimumCut FindMinValue FindPermutation FindPostmanTour FindProcessParameters FindRoot FindSequenceFunction FindSettings FindShortestPath FindShortestTour FindThreshold FindVertexCover FindVertexCut Fine FinishDynamic FiniteAbelianGroupCount FiniteGroupCount FiniteGroupData First FirstPassageTimeDistribution FischerGroupFi22 FischerGroupFi23 FischerGroupFi24Prime FisherHypergeometricDistribution FisherRatioTest FisherZDistribution Fit FitAll FittedModel FixedPoint FixedPointList FlashSelection Flat Flatten FlattenAt FlatTopWindow FlipView Floor FlushPrintOutputPacket Fold FoldList Font FontColor FontFamily FontForm FontName FontOpacity FontPostScriptName FontProperties FontReencoding FontSize FontSlant FontSubstitutions FontTracking FontVariations FontWeight For ForAll Format FormatRules FormatType FormatTypeAutoConvert FormatValues FormBox FormBoxOptions FortranForm Forward ForwardBackward Fourier FourierCoefficient FourierCosCoefficient FourierCosSeries FourierCosTransform FourierDCT FourierDCTFilter FourierDCTMatrix FourierDST FourierDSTMatrix FourierMatrix FourierParameters FourierSequenceTransform FourierSeries FourierSinCoefficient FourierSinSeries FourierSinTransform FourierTransform FourierTrigSeries FractionalBrownianMotionProcess FractionalPart FractionBox FractionBoxOptions FractionLine Frame FrameBox FrameBoxOptions Framed FrameInset FrameLabel Frameless FrameMargins FrameStyle FrameTicks FrameTicksStyle FRatioDistribution FrechetDistribution FreeQ FrequencySamplingFilterKernel FresnelC FresnelS Friday FrobeniusNumber FrobeniusSolve FromCharacterCode FromCoefficientRules FromContinuedFraction FromDate FromDigits FromDMS Front FrontEndDynamicExpression FrontEndEventActions FrontEndExecute FrontEndObject FrontEndResource FrontEndResourceString FrontEndStackSize FrontEndToken FrontEndTokenExecute FrontEndValueCache FrontEndVersion FrontFaceColor FrontFaceOpacity Full FullAxes FullDefinition FullForm FullGraphics FullOptions FullSimplify Function FunctionExpand FunctionInterpolation FunctionSpace FussellVeselyImportance GaborFilter GaborMatrix GaborWavelet GainMargins GainPhaseMargins Gamma GammaDistribution GammaRegularized GapPenalty Gather GatherBy GaugeFaceElementFunction GaugeFaceStyle GaugeFrameElementFunction GaugeFrameSize GaugeFrameStyle GaugeLabels GaugeMarkers GaugeStyle GaussianFilter GaussianIntegers GaussianMatrix GaussianWindow GCD GegenbauerC General GeneralizedLinearModelFit GenerateConditions GeneratedCell GeneratedParameters GeneratingFunction Generic GenericCylindricalDecomposition GenomeData GenomeLookup GeodesicClosing GeodesicDilation GeodesicErosion GeodesicOpening GeoDestination GeodesyData GeoDirection GeoDistance GeoGridPosition GeometricBrownianMotionProcess GeometricDistribution GeometricMean GeometricMeanFilter GeometricTransformation GeometricTransformation3DBox GeometricTransformation3DBoxOptions GeometricTransformationBox GeometricTransformationBoxOptions GeoPosition GeoPositionENU GeoPositionXYZ GeoProjectionData GestureHandler GestureHandlerTag Get GetBoundingBoxSizePacket GetContext GetEnvironment GetFileName GetFrontEndOptionsDataPacket GetLinebreakInformationPacket GetMenusPacket GetPageBreakInformationPacket Glaisher GlobalClusteringCoefficient GlobalPreferences GlobalSession Glow GoldenRatio GompertzMakehamDistribution GoodmanKruskalGamma GoodmanKruskalGammaTest Goto Grad Gradient GradientFilter GradientOrientationFilter Graph GraphAssortativity GraphCenter GraphComplement GraphData GraphDensity GraphDiameter GraphDifference GraphDisjointUnion GraphDistance GraphDistanceMatrix GraphElementData GraphEmbedding GraphHighlight GraphHighlightStyle GraphHub Graphics Graphics3D Graphics3DBox Graphics3DBoxOptions GraphicsArray GraphicsBaseline GraphicsBox GraphicsBoxOptions GraphicsColor GraphicsColumn GraphicsComplex GraphicsComplex3DBox GraphicsComplex3DBoxOptions GraphicsComplexBox GraphicsComplexBoxOptions GraphicsContents GraphicsData GraphicsGrid GraphicsGridBox GraphicsGroup GraphicsGroup3DBox GraphicsGroup3DBoxOptions GraphicsGroupBox GraphicsGroupBoxOptions GraphicsGrouping GraphicsHighlightColor GraphicsRow GraphicsSpacing GraphicsStyle GraphIntersection GraphLayout GraphLinkEfficiency GraphPeriphery GraphPlot GraphPlot3D GraphPower GraphPropertyDistribution GraphQ GraphRadius GraphReciprocity GraphRoot GraphStyle GraphUnion Gray GrayLevel GreatCircleDistance Greater GreaterEqual GreaterEqualLess GreaterFullEqual GreaterGreater GreaterLess GreaterSlantEqual GreaterTilde Green Grid GridBaseline GridBox GridBoxAlignment GridBoxBackground GridBoxDividers GridBoxFrame GridBoxItemSize GridBoxItemStyle GridBoxOptions GridBoxSpacings GridCreationSettings GridDefaultElement GridElementStyleOptions GridFrame GridFrameMargins GridGraph GridLines GridLinesStyle GroebnerBasis GroupActionBase GroupCentralizer GroupElementFromWord GroupElementPosition GroupElementQ GroupElements GroupElementToWord GroupGenerators GroupMultiplicationTable GroupOrbits GroupOrder GroupPageBreakWithin GroupSetwiseStabilizer GroupStabilizer GroupStabilizerChain Gudermannian GumbelDistribution HaarWavelet HadamardMatrix HalfNormalDistribution HamiltonianGraphQ HammingDistance HammingWindow HankelH1 HankelH2 HankelMatrix HannPoissonWindow HannWindow HaradaNortonGroupHN HararyGraph HarmonicMean HarmonicMeanFilter HarmonicNumber Hash HashTable Haversine HazardFunction Head HeadCompose Heads HeavisideLambda HeavisidePi HeavisideTheta HeldGroupHe HeldPart HelpBrowserLookup HelpBrowserNotebook HelpBrowserSettings HermiteDecomposition HermiteH HermitianMatrixQ HessenbergDecomposition Hessian HexadecimalCharacter Hexahedron HexahedronBox HexahedronBoxOptions HiddenSurface HighlightGraph HighlightImage HighpassFilter HigmanSimsGroupHS HilbertFilter HilbertMatrix Histogram Histogram3D HistogramDistribution HistogramList HistogramTransform HistogramTransformInterpolation HitMissTransform HITSCentrality HodgeDual HoeffdingD HoeffdingDTest Hold HoldAll HoldAllComplete HoldComplete HoldFirst HoldForm HoldPattern HoldRest HolidayCalendar HomeDirectory HomePage Horizontal HorizontalForm HorizontalGauge HorizontalScrollPosition HornerForm HotellingTSquareDistribution HoytDistribution HTMLSave Hue HumpDownHump HumpEqual HurwitzLerchPhi HurwitzZeta HyperbolicDistribution HypercubeGraph HyperexponentialDistribution Hyperfactorial Hypergeometric0F1 Hypergeometric0F1Regularized Hypergeometric1F1 Hypergeometric1F1Regularized Hypergeometric2F1 Hypergeometric2F1Regularized HypergeometricDistribution HypergeometricPFQ HypergeometricPFQRegularized HypergeometricU Hyperlink HyperlinkCreationSettings Hyphenation HyphenationOptions HypoexponentialDistribution HypothesisTestData I Identity IdentityMatrix If IgnoreCase Im Image Image3D Image3DSlices ImageAccumulate ImageAdd ImageAdjust ImageAlign ImageApply ImageAspectRatio ImageAssemble ImageCache ImageCacheValid ImageCapture ImageChannels ImageClip ImageColorSpace ImageCompose ImageConvolve ImageCooccurrence ImageCorners ImageCorrelate ImageCorrespondingPoints ImageCrop ImageData ImageDataPacket ImageDeconvolve ImageDemosaic ImageDifference ImageDimensions ImageDistance ImageEffect ImageFeatureTrack ImageFileApply ImageFileFilter ImageFileScan ImageFilter ImageForestingComponents ImageForwardTransformation ImageHistogram ImageKeypoints ImageLevels ImageLines ImageMargins ImageMarkers ImageMeasurements ImageMultiply ImageOffset ImagePad ImagePadding ImagePartition ImagePeriodogram ImagePerspectiveTransformation ImageQ ImageRangeCache ImageReflect ImageRegion ImageResize ImageResolution ImageRotate ImageRotated ImageScaled ImageScan ImageSize ImageSizeAction ImageSizeCache ImageSizeMultipliers ImageSizeRaw ImageSubtract ImageTake ImageTransformation ImageTrim ImageType ImageValue ImageValuePositions Implies Import ImportAutoReplacements ImportString ImprovementImportance In IncidenceGraph IncidenceList IncidenceMatrix IncludeConstantBasis IncludeFileExtension IncludePods IncludeSingularTerm Increment Indent IndentingNewlineSpacings IndentMaxFraction IndependenceTest IndependentEdgeSetQ IndependentUnit IndependentVertexSetQ Indeterminate IndexCreationOptions Indexed IndexGraph IndexTag Inequality InexactNumberQ InexactNumbers Infinity Infix Information Inherited InheritScope Initialization InitializationCell InitializationCellEvaluation InitializationCellWarning InlineCounterAssignments InlineCounterIncrements InlineRules Inner Inpaint Input InputAliases InputAssumptions InputAutoReplacements InputField InputFieldBox InputFieldBoxOptions InputForm InputGrouping InputNamePacket InputNotebook InputPacket InputSettings InputStream InputString InputStringPacket InputToBoxFormPacket Insert InsertionPointObject InsertResults Inset Inset3DBox Inset3DBoxOptions InsetBox InsetBoxOptions Install InstallService InString Integer IntegerDigits IntegerExponent IntegerLength IntegerPart IntegerPartitions IntegerQ Integers IntegerString Integral Integrate Interactive InteractiveTradingChart Interlaced Interleaving InternallyBalancedDecomposition InterpolatingFunction InterpolatingPolynomial Interpolation InterpolationOrder InterpolationPoints InterpolationPrecision Interpretation InterpretationBox InterpretationBoxOptions InterpretationFunction InterpretTemplate InterquartileRange Interrupt InterruptSettings Intersection Interval IntervalIntersection IntervalMemberQ IntervalUnion Inverse InverseBetaRegularized InverseCDF InverseChiSquareDistribution InverseContinuousWaveletTransform InverseDistanceTransform InverseEllipticNomeQ InverseErf InverseErfc InverseFourier InverseFourierCosTransform InverseFourierSequenceTransform InverseFourierSinTransform InverseFourierTransform InverseFunction InverseFunctions InverseGammaDistribution InverseGammaRegularized InverseGaussianDistribution InverseGudermannian InverseHaversine InverseJacobiCD InverseJacobiCN InverseJacobiCS InverseJacobiDC InverseJacobiDN InverseJacobiDS InverseJacobiNC InverseJacobiND InverseJacobiNS InverseJacobiSC InverseJacobiSD InverseJacobiSN InverseLaplaceTransform InversePermutation InverseRadon InverseSeries InverseSurvivalFunction InverseWaveletTransform InverseWeierstrassP InverseZTransform Invisible InvisibleApplication InvisibleTimes IrreduciblePolynomialQ IsolatingInterval IsomorphicGraphQ IsotopeData Italic Item ItemBox ItemBoxOptions ItemSize ItemStyle ItoProcess JaccardDissimilarity JacobiAmplitude Jacobian JacobiCD JacobiCN JacobiCS JacobiDC JacobiDN JacobiDS JacobiNC JacobiND JacobiNS JacobiP JacobiSC JacobiSD JacobiSN JacobiSymbol JacobiZeta JankoGroupJ1 JankoGroupJ2 JankoGroupJ3 JankoGroupJ4 JarqueBeraALMTest JohnsonDistribution Join Joined JoinedCurve JoinedCurveBox JoinForm JordanDecomposition JordanModelDecomposition K KagiChart KaiserBesselWindow KaiserWindow KalmanEstimator KalmanFilter KarhunenLoeveDecomposition KaryTree KatzCentrality KCoreComponents KDistribution KelvinBei KelvinBer KelvinKei KelvinKer KendallTau KendallTauTest KernelExecute KernelMixtureDistribution KernelObject Kernels Ket Khinchin KirchhoffGraph KirchhoffMatrix KleinInvariantJ KnightTourGraph KnotData KnownUnitQ KolmogorovSmirnovTest KroneckerDelta KroneckerModelDecomposition KroneckerProduct KroneckerSymbol KuiperTest KumaraswamyDistribution Kurtosis KuwaharaFilter Label Labeled LabeledSlider LabelingFunction LabelStyle LaguerreL LambdaComponents LambertW LanczosWindow LandauDistribution Language LanguageCategory LaplaceDistribution LaplaceTransform Laplacian LaplacianFilter LaplacianGaussianFilter Large Larger Last Latitude LatitudeLongitude LatticeData LatticeReduce Launch LaunchKernels LayeredGraphPlot LayerSizeFunction LayoutInformation LCM LeafCount LeapYearQ LeastSquares LeastSquaresFilterKernel Left LeftArrow LeftArrowBar LeftArrowRightArrow LeftDownTeeVector LeftDownVector LeftDownVectorBar LeftRightArrow LeftRightVector LeftTee LeftTeeArrow LeftTeeVector LeftTriangle LeftTriangleBar LeftTriangleEqual LeftUpDownVector LeftUpTeeVector LeftUpVector LeftUpVectorBar LeftVector LeftVectorBar LegendAppearance Legended LegendFunction LegendLabel LegendLayout LegendMargins LegendMarkers LegendMarkerSize LegendreP LegendreQ LegendreType Length LengthWhile LerchPhi Less LessEqual LessEqualGreater LessFullEqual LessGreater LessLess LessSlantEqual LessTilde LetterCharacter LetterQ Level LeveneTest LeviCivitaTensor LevyDistribution Lexicographic LibraryFunction LibraryFunctionError LibraryFunctionInformation LibraryFunctionLoad LibraryFunctionUnload LibraryLoad LibraryUnload LicenseID LiftingFilterData LiftingWaveletTransform LightBlue LightBrown LightCyan Lighter LightGray LightGreen Lighting LightingAngle LightMagenta LightOrange LightPink LightPurple LightRed LightSources LightYellow Likelihood Limit LimitsPositioning LimitsPositioningTokens LindleyDistribution Line Line3DBox LinearFilter LinearFractionalTransform LinearModelFit LinearOffsetFunction LinearProgramming LinearRecurrence LinearSolve LinearSolveFunction LineBox LineBreak LinebreakAdjustments LineBreakChart LineBreakWithin LineColor LineForm LineGraph LineIndent LineIndentMaxFraction LineIntegralConvolutionPlot LineIntegralConvolutionScale LineLegend LineOpacity LineSpacing LineWrapParts LinkActivate LinkClose LinkConnect LinkConnectedQ LinkCreate LinkError LinkFlush LinkFunction LinkHost LinkInterrupt LinkLaunch LinkMode LinkObject LinkOpen LinkOptions LinkPatterns LinkProtocol LinkRead LinkReadHeld LinkReadyQ Links LinkWrite LinkWriteHeld LiouvilleLambda List Listable ListAnimate ListContourPlot ListContourPlot3D ListConvolve ListCorrelate ListCurvePathPlot ListDeconvolve ListDensityPlot Listen ListFourierSequenceTransform ListInterpolation ListLineIntegralConvolutionPlot ListLinePlot ListLogLinearPlot ListLogLogPlot ListLogPlot ListPicker ListPickerBox ListPickerBoxBackground ListPickerBoxOptions ListPlay ListPlot ListPlot3D ListPointPlot3D ListPolarPlot ListQ ListStreamDensityPlot ListStreamPlot ListSurfacePlot3D ListVectorDensityPlot ListVectorPlot ListVectorPlot3D ListZTransform Literal LiteralSearch LocalClusteringCoefficient LocalizeVariables LocationEquivalenceTest LocationTest Locator LocatorAutoCreate LocatorBox LocatorBoxOptions LocatorCentering LocatorPane LocatorPaneBox LocatorPaneBoxOptions LocatorRegion Locked Log Log10 Log2 LogBarnesG LogGamma LogGammaDistribution LogicalExpand LogIntegral LogisticDistribution LogitModelFit LogLikelihood LogLinearPlot LogLogisticDistribution LogLogPlot LogMultinormalDistribution LogNormalDistribution LogPlot LogRankTest LogSeriesDistribution LongEqual Longest LongestAscendingSequence LongestCommonSequence LongestCommonSequencePositions LongestCommonSubsequence LongestCommonSubsequencePositions LongestMatch LongForm Longitude LongLeftArrow LongLeftRightArrow LongRightArrow Loopback LoopFreeGraphQ LowerCaseQ LowerLeftArrow LowerRightArrow LowerTriangularize LowpassFilter LQEstimatorGains LQGRegulator LQOutputRegulatorGains LQRegulatorGains LUBackSubstitution LucasL LuccioSamiComponents LUDecomposition LyapunovSolve LyonsGroupLy MachineID MachineName MachineNumberQ MachinePrecision MacintoshSystemPageSetup Magenta Magnification Magnify MainSolve MaintainDynamicCaches Majority MakeBoxes MakeExpression MakeRules MangoldtLambda ManhattanDistance Manipulate Manipulator MannWhitneyTest MantissaExponent Manual Map MapAll MapAt MapIndexed MAProcess MapThread MarcumQ MardiaCombinedTest MardiaKurtosisTest MardiaSkewnessTest MarginalDistribution MarkovProcessProperties Masking MatchingDissimilarity MatchLocalNameQ MatchLocalNames MatchQ Material MathematicaNotation MathieuC MathieuCharacteristicA MathieuCharacteristicB MathieuCharacteristicExponent MathieuCPrime MathieuGroupM11 MathieuGroupM12 MathieuGroupM22 MathieuGroupM23 MathieuGroupM24 MathieuS MathieuSPrime MathMLForm MathMLText Matrices MatrixExp MatrixForm MatrixFunction MatrixLog MatrixPlot MatrixPower MatrixQ MatrixRank Max MaxBend MaxDetect MaxExtraBandwidths MaxExtraConditions MaxFeatures MaxFilter Maximize MaxIterations MaxMemoryUsed MaxMixtureKernels MaxPlotPoints MaxPoints MaxRecursion MaxStableDistribution MaxStepFraction MaxSteps MaxStepSize MaxValue MaxwellDistribution McLaughlinGroupMcL Mean MeanClusteringCoefficient MeanDegreeConnectivity MeanDeviation MeanFilter MeanGraphDistance MeanNeighborDegree MeanShift MeanShiftFilter Median MedianDeviation MedianFilter Medium MeijerG MeixnerDistribution MemberQ MemoryConstrained MemoryInUse Menu MenuAppearance MenuCommandKey MenuEvaluator MenuItem MenuPacket MenuSortingValue MenuStyle MenuView MergeDifferences Mesh MeshFunctions MeshRange MeshShading MeshStyle Message MessageDialog MessageList MessageName MessageOptions MessagePacket Messages MessagesNotebook MetaCharacters MetaInformation Method MethodOptions MexicanHatWavelet MeyerWavelet Min MinDetect MinFilter MinimalPolynomial MinimalStateSpaceModel Minimize Minors MinRecursion MinSize MinStableDistribution Minus MinusPlus MinValue Missing MissingDataMethod MittagLefflerE MixedRadix MixedRadixQuantity MixtureDistribution Mod Modal Mode Modular ModularLambda Module Modulus MoebiusMu Moment Momentary MomentConvert MomentEvaluate MomentGeneratingFunction Monday Monitor MonomialList MonomialOrder MonsterGroupM MorletWavelet MorphologicalBinarize MorphologicalBranchPoints MorphologicalComponents MorphologicalEulerNumber MorphologicalGraph MorphologicalPerimeter MorphologicalTransform Most MouseAnnotation MouseAppearance MouseAppearanceTag MouseButtons Mouseover MousePointerNote MousePosition MovingAverage MovingMedian MoyalDistribution MultiedgeStyle MultilaunchWarning MultiLetterItalics MultiLetterStyle MultilineFunction Multinomial MultinomialDistribution MultinormalDistribution MultiplicativeOrder Multiplicity Multiselection MultivariateHypergeometricDistribution MultivariatePoissonDistribution MultivariateTDistribution N NakagamiDistribution NameQ Names NamespaceBox Nand NArgMax NArgMin NBernoulliB NCache NDSolve NDSolveValue Nearest NearestFunction NeedCurrentFrontEndPackagePacket NeedCurrentFrontEndSymbolsPacket NeedlemanWunschSimilarity Needs Negative NegativeBinomialDistribution NegativeMultinomialDistribution NeighborhoodGraph Nest NestedGreaterGreater NestedLessLess NestedScriptRules NestList NestWhile NestWhileList NevilleThetaC NevilleThetaD NevilleThetaN NevilleThetaS NewPrimitiveStyle NExpectation Next NextPrime NHoldAll NHoldFirst NHoldRest NicholsGridLines NicholsPlot NIntegrate NMaximize NMaxValue NMinimize NMinValue NominalVariables NonAssociative NoncentralBetaDistribution NoncentralChiSquareDistribution NoncentralFRatioDistribution NoncentralStudentTDistribution NonCommutativeMultiply NonConstants None NonlinearModelFit NonlocalMeansFilter NonNegative NonPositive Nor NorlundB Norm Normal NormalDistribution NormalGrouping Normalize NormalizedSquaredEuclideanDistance NormalsFunction NormFunction Not NotCongruent NotCupCap NotDoubleVerticalBar Notebook NotebookApply NotebookAutoSave NotebookClose NotebookConvertSettings NotebookCreate NotebookCreateReturnObject NotebookDefault NotebookDelete NotebookDirectory NotebookDynamicExpression NotebookEvaluate NotebookEventActions NotebookFileName NotebookFind NotebookFindReturnObject NotebookGet NotebookGetLayoutInformationPacket NotebookGetMisspellingsPacket NotebookInformation NotebookInterfaceObject NotebookLocate NotebookObject NotebookOpen NotebookOpenReturnObject NotebookPath NotebookPrint NotebookPut NotebookPutReturnObject NotebookRead NotebookResetGeneratedCells Notebooks NotebookSave NotebookSaveAs NotebookSelection NotebookSetupLayoutInformationPacket NotebooksMenu NotebookWrite NotElement NotEqualTilde NotExists NotGreater NotGreaterEqual NotGreaterFullEqual NotGreaterGreater NotGreaterLess NotGreaterSlantEqual NotGreaterTilde NotHumpDownHump NotHumpEqual NotLeftTriangle NotLeftTriangleBar NotLeftTriangleEqual NotLess NotLessEqual NotLessFullEqual NotLessGreater NotLessLess NotLessSlantEqual NotLessTilde NotNestedGreaterGreater NotNestedLessLess NotPrecedes NotPrecedesEqual NotPrecedesSlantEqual NotPrecedesTilde NotReverseElement NotRightTriangle NotRightTriangleBar NotRightTriangleEqual NotSquareSubset NotSquareSubsetEqual NotSquareSuperset NotSquareSupersetEqual NotSubset NotSubsetEqual NotSucceeds NotSucceedsEqual NotSucceedsSlantEqual NotSucceedsTilde NotSuperset NotSupersetEqual NotTilde NotTildeEqual NotTildeFullEqual NotTildeTilde NotVerticalBar NProbability NProduct NProductFactors NRoots NSolve NSum NSumTerms Null NullRecords NullSpace NullWords Number NumberFieldClassNumber NumberFieldDiscriminant NumberFieldFundamentalUnits NumberFieldIntegralBasis NumberFieldNormRepresentatives NumberFieldRegulator NumberFieldRootsOfUnity NumberFieldSignature NumberForm NumberFormat NumberMarks NumberMultiplier NumberPadding NumberPoint NumberQ NumberSeparator NumberSigns NumberString Numerator NumericFunction NumericQ NuttallWindow NValues NyquistGridLines NyquistPlot O ObservabilityGramian ObservabilityMatrix ObservableDecomposition ObservableModelQ OddQ Off Offset OLEData On ONanGroupON OneIdentity Opacity Open OpenAppend Opener OpenerBox OpenerBoxOptions OpenerView OpenFunctionInspectorPacket Opening OpenRead OpenSpecialOptions OpenTemporary OpenWrite Operate OperatingSystem OptimumFlowData Optional OptionInspectorSettings OptionQ Options OptionsPacket OptionsPattern OptionValue OptionValueBox OptionValueBoxOptions Or Orange Order OrderDistribution OrderedQ Ordering Orderless OrnsteinUhlenbeckProcess Orthogonalize Out Outer OutputAutoOverwrite OutputControllabilityMatrix OutputControllableModelQ OutputForm OutputFormData OutputGrouping OutputMathEditExpression OutputNamePacket OutputResponse OutputSizeLimit OutputStream Over OverBar OverDot Overflow OverHat Overlaps Overlay OverlayBox OverlayBoxOptions Overscript OverscriptBox OverscriptBoxOptions OverTilde OverVector OwenT OwnValues PackingMethod PaddedForm Padding PadeApproximant PadLeft PadRight PageBreakAbove PageBreakBelow PageBreakWithin PageFooterLines PageFooters PageHeaderLines PageHeaders PageHeight PageRankCentrality PageWidth PairedBarChart PairedHistogram PairedSmoothHistogram PairedTTest PairedZTest PaletteNotebook PalettePath Pane PaneBox PaneBoxOptions Panel PanelBox PanelBoxOptions Paneled PaneSelector PaneSelectorBox PaneSelectorBoxOptions PaperWidth ParabolicCylinderD ParagraphIndent ParagraphSpacing ParallelArray ParallelCombine ParallelDo ParallelEvaluate Parallelization Parallelize ParallelMap ParallelNeeds ParallelProduct ParallelSubmit ParallelSum ParallelTable ParallelTry Parameter ParameterEstimator ParameterMixtureDistribution ParameterVariables ParametricFunction ParametricNDSolve ParametricNDSolveValue ParametricPlot ParametricPlot3D ParentConnect ParentDirectory ParentForm Parenthesize ParentList ParetoDistribution Part PartialCorrelationFunction PartialD ParticleData Partition PartitionsP PartitionsQ ParzenWindow PascalDistribution PassEventsDown PassEventsUp Paste PasteBoxFormInlineCells PasteButton Path PathGraph PathGraphQ Pattern PatternSequence PatternTest PauliMatrix PaulWavelet Pause PausedTime PDF PearsonChiSquareTest PearsonCorrelationTest PearsonDistribution PerformanceGoal PeriodicInterpolation Periodogram PeriodogramArray PermutationCycles PermutationCyclesQ PermutationGroup PermutationLength PermutationList PermutationListQ PermutationMax PermutationMin PermutationOrder PermutationPower PermutationProduct PermutationReplace Permutations PermutationSupport Permute PeronaMalikFilter Perpendicular PERTDistribution PetersenGraph PhaseMargins Pi Pick PIDData PIDDerivativeFilter PIDFeedforward PIDTune Piecewise PiecewiseExpand PieChart PieChart3D PillaiTrace PillaiTraceTest Pink Pivoting PixelConstrained PixelValue PixelValuePositions Placed Placeholder PlaceholderReplace Plain PlanarGraphQ Play PlayRange Plot Plot3D Plot3Matrix PlotDivision PlotJoined PlotLabel PlotLayout PlotLegends PlotMarkers PlotPoints PlotRange PlotRangeClipping PlotRangePadding PlotRegion PlotStyle Plus PlusMinus Pochhammer PodStates PodWidth Point Point3DBox PointBox PointFigureChart PointForm PointLegend PointSize PoissonConsulDistribution PoissonDistribution PoissonProcess PoissonWindow PolarAxes PolarAxesOrigin PolarGridLines PolarPlot PolarTicks PoleZeroMarkers PolyaAeppliDistribution PolyGamma Polygon Polygon3DBox Polygon3DBoxOptions PolygonBox PolygonBoxOptions PolygonHoleScale PolygonIntersections PolygonScale PolyhedronData PolyLog PolynomialExtendedGCD PolynomialForm PolynomialGCD PolynomialLCM PolynomialMod PolynomialQ PolynomialQuotient PolynomialQuotientRemainder PolynomialReduce PolynomialRemainder Polynomials PopupMenu PopupMenuBox PopupMenuBoxOptions PopupView PopupWindow Position Positive PositiveDefiniteMatrixQ PossibleZeroQ Postfix PostScript Power PowerDistribution PowerExpand PowerMod PowerModList PowerSpectralDensity PowersRepresentations PowerSymmetricPolynomial Precedence PrecedenceForm Precedes PrecedesEqual PrecedesSlantEqual PrecedesTilde Precision PrecisionGoal PreDecrement PredictionRoot PreemptProtect PreferencesPath Prefix PreIncrement Prepend PrependTo PreserveImageOptions Previous PriceGraphDistribution PrimaryPlaceholder Prime PrimeNu PrimeOmega PrimePi PrimePowerQ PrimeQ Primes PrimeZetaP PrimitiveRoot PrincipalComponents PrincipalValue Print PrintAction PrintForm PrintingCopies PrintingOptions PrintingPageRange PrintingStartingPageNumber PrintingStyleEnvironment PrintPrecision PrintTemporary Prism PrismBox PrismBoxOptions PrivateCellOptions PrivateEvaluationOptions PrivateFontOptions PrivateFrontEndOptions PrivateNotebookOptions PrivatePaths Probability ProbabilityDistribution ProbabilityPlot ProbabilityPr ProbabilityScalePlot ProbitModelFit ProcessEstimator ProcessParameterAssumptions ProcessParameterQ ProcessStateDomain ProcessTimeDomain Product ProductDistribution ProductLog ProgressIndicator ProgressIndicatorBox ProgressIndicatorBoxOptions Projection Prolog PromptForm Properties Property PropertyList PropertyValue Proportion Proportional Protect Protected ProteinData Pruning PseudoInverse Purple Put PutAppend Pyramid PyramidBox PyramidBoxOptions QBinomial QFactorial QGamma QHypergeometricPFQ QPochhammer QPolyGamma QRDecomposition QuadraticIrrationalQ Quantile QuantilePlot Quantity QuantityForm QuantityMagnitude QuantityQ QuantityUnit Quartics QuartileDeviation Quartiles QuartileSkewness QueueingNetworkProcess QueueingProcess QueueProperties Quiet Quit Quotient QuotientRemainder RadialityCentrality RadicalBox RadicalBoxOptions RadioButton RadioButtonBar RadioButtonBox RadioButtonBoxOptions Radon RamanujanTau RamanujanTauL RamanujanTauTheta RamanujanTauZ Random RandomChoice RandomComplex RandomFunction RandomGraph RandomImage RandomInteger RandomPermutation RandomPrime RandomReal RandomSample RandomSeed RandomVariate RandomWalkProcess Range RangeFilter RangeSpecification RankedMax RankedMin Raster Raster3D Raster3DBox Raster3DBoxOptions RasterArray RasterBox RasterBoxOptions Rasterize RasterSize Rational RationalFunctions Rationalize Rationals Ratios Raw RawArray RawBoxes RawData RawMedium RayleighDistribution Re Read ReadList ReadProtected Real RealBlockDiagonalForm RealDigits RealExponent Reals Reap Record RecordLists RecordSeparators Rectangle RectangleBox RectangleBoxOptions RectangleChart RectangleChart3D RecurrenceFilter RecurrenceTable RecurringDigitsForm Red Reduce RefBox ReferenceLineStyle ReferenceMarkers ReferenceMarkerStyle Refine ReflectionMatrix ReflectionTransform Refresh RefreshRate RegionBinarize RegionFunction RegionPlot RegionPlot3D RegularExpression Regularization Reinstall Release ReleaseHold ReliabilityDistribution ReliefImage ReliefPlot Remove RemoveAlphaChannel RemoveAsynchronousTask Removed RemoveInputStreamMethod RemoveOutputStreamMethod RemoveProperty RemoveScheduledTask RenameDirectory RenameFile RenderAll RenderingOptions RenewalProcess RenkoChart Repeated RepeatedNull RepeatedString Replace ReplaceAll ReplaceHeldPart ReplaceImageValue ReplaceList ReplacePart ReplacePixelValue ReplaceRepeated Resampling Rescale RescalingTransform ResetDirectory ResetMenusPacket ResetScheduledTask Residue Resolve Rest Resultant ResumePacket Return ReturnExpressionPacket ReturnInputFormPacket ReturnPacket ReturnTextPacket Reverse ReverseBiorthogonalSplineWavelet ReverseElement ReverseEquilibrium ReverseGraph ReverseUpEquilibrium RevolutionAxis RevolutionPlot3D RGBColor RiccatiSolve RiceDistribution RidgeFilter RiemannR RiemannSiegelTheta RiemannSiegelZ Riffle Right RightArrow RightArrowBar RightArrowLeftArrow RightCosetRepresentative RightDownTeeVector RightDownVector RightDownVectorBar RightTee RightTeeArrow RightTeeVector RightTriangle RightTriangleBar RightTriangleEqual RightUpDownVector RightUpTeeVector RightUpVector RightUpVectorBar RightVector RightVectorBar RiskAchievementImportance RiskReductionImportance RogersTanimotoDissimilarity Root RootApproximant RootIntervals RootLocusPlot RootMeanSquare RootOfUnityQ RootReduce Roots RootSum Rotate RotateLabel RotateLeft RotateRight RotationAction RotationBox RotationBoxOptions RotationMatrix RotationTransform Round RoundImplies RoundingRadius Row RowAlignments RowBackgrounds RowBox RowHeights RowLines RowMinHeight RowReduce RowsEqual RowSpacings RSolve RudvalisGroupRu Rule RuleCondition RuleDelayed RuleForm RulerUnits Run RunScheduledTask RunThrough RuntimeAttributes RuntimeOptions RussellRaoDissimilarity SameQ SameTest SampleDepth SampledSoundFunction SampledSoundList SampleRate SamplingPeriod SARIMAProcess SARMAProcess SatisfiabilityCount SatisfiabilityInstances SatisfiableQ Saturday Save Saveable SaveAutoDelete SaveDefinitions SawtoothWave Scale Scaled ScaleDivisions ScaledMousePosition ScaleOrigin ScalePadding ScaleRanges ScaleRangeStyle ScalingFunctions ScalingMatrix ScalingTransform Scan ScheduledTaskActiveQ ScheduledTaskData ScheduledTaskObject ScheduledTasks SchurDecomposition ScientificForm ScreenRectangle ScreenStyleEnvironment ScriptBaselineShifts ScriptLevel ScriptMinSize ScriptRules ScriptSizeMultipliers Scrollbars ScrollingOptions ScrollPosition Sec Sech SechDistribution SectionGrouping SectorChart SectorChart3D SectorOrigin SectorSpacing SeedRandom Select Selectable SelectComponents SelectedCells SelectedNotebook Selection SelectionAnimate SelectionCell SelectionCellCreateCell SelectionCellDefaultStyle SelectionCellParentStyle SelectionCreateCell SelectionDebuggerTag SelectionDuplicateCell SelectionEvaluate SelectionEvaluateCreateCell SelectionMove SelectionPlaceholder SelectionSetStyle SelectWithContents SelfLoops SelfLoopStyle SemialgebraicComponentInstances SendMail Sequence SequenceAlignment SequenceForm SequenceHold SequenceLimit Series SeriesCoefficient SeriesData SessionTime Set SetAccuracy SetAlphaChannel SetAttributes Setbacks SetBoxFormNamesPacket SetDelayed SetDirectory SetEnvironment SetEvaluationNotebook SetFileDate SetFileLoadingContext SetNotebookStatusLine SetOptions SetOptionsPacket SetPrecision SetProperty SetSelectedNotebook SetSharedFunction SetSharedVariable SetSpeechParametersPacket SetStreamPosition SetSystemOptions Setter SetterBar SetterBox SetterBoxOptions Setting SetValue Shading Shallow ShannonWavelet ShapiroWilkTest Share Sharpen ShearingMatrix ShearingTransform ShenCastanMatrix Short ShortDownArrow Shortest ShortestMatch ShortestPathFunction ShortLeftArrow ShortRightArrow ShortUpArrow Show ShowAutoStyles ShowCellBracket ShowCellLabel ShowCellTags ShowClosedCellArea ShowContents ShowControls ShowCursorTracker ShowGroupOpenCloseIcon ShowGroupOpener ShowInvisibleCharacters ShowPageBreaks ShowPredictiveInterface ShowSelection ShowShortBoxForm ShowSpecialCharacters ShowStringCharacters ShowSyntaxStyles ShrinkingDelay ShrinkWrapBoundingBox SiegelTheta SiegelTukeyTest Sign Signature SignedRankTest SignificanceLevel SignPadding SignTest SimilarityRules SimpleGraph SimpleGraphQ Simplify Sin Sinc SinghMaddalaDistribution SingleEvaluation SingleLetterItalics SingleLetterStyle SingularValueDecomposition SingularValueList SingularValuePlot SingularValues Sinh SinhIntegral SinIntegral SixJSymbol Skeleton SkeletonTransform SkellamDistribution Skewness SkewNormalDistribution Skip SliceDistribution Slider Slider2D Slider2DBox Slider2DBoxOptions SliderBox SliderBoxOptions SlideView Slot SlotSequence Small SmallCircle Smaller SmithDelayCompensator SmithWatermanSimilarity SmoothDensityHistogram SmoothHistogram SmoothHistogram3D SmoothKernelDistribution SocialMediaData Socket SokalSneathDissimilarity Solve SolveAlways SolveDelayed Sort SortBy Sound SoundAndGraphics SoundNote SoundVolume Sow Space SpaceForm Spacer Spacings Span SpanAdjustments SpanCharacterRounding SpanFromAbove SpanFromBoth SpanFromLeft SpanLineThickness SpanMaxSize SpanMinSize SpanningCharacters SpanSymmetric SparseArray SpatialGraphDistribution Speak SpeakTextPacket SpearmanRankTest SpearmanRho Spectrogram SpectrogramArray Specularity SpellingCorrection SpellingDictionaries SpellingDictionariesPath SpellingOptions SpellingSuggestionsPacket Sphere SphereBox SphericalBesselJ SphericalBesselY SphericalHankelH1 SphericalHankelH2 SphericalHarmonicY SphericalPlot3D SphericalRegion SpheroidalEigenvalue SpheroidalJoiningFactor SpheroidalPS SpheroidalPSPrime SpheroidalQS SpheroidalQSPrime SpheroidalRadialFactor SpheroidalS1 SpheroidalS1Prime SpheroidalS2 SpheroidalS2Prime Splice SplicedDistribution SplineClosed SplineDegree SplineKnots SplineWeights Split SplitBy SpokenString Sqrt SqrtBox SqrtBoxOptions Square SquaredEuclideanDistance SquareFreeQ SquareIntersection SquaresR SquareSubset SquareSubsetEqual SquareSuperset SquareSupersetEqual SquareUnion SquareWave StabilityMargins StabilityMarginsStyle StableDistribution Stack StackBegin StackComplete StackInhibit StandardDeviation StandardDeviationFilter StandardForm Standardize StandbyDistribution Star StarGraph StartAsynchronousTask StartingStepSize StartOfLine StartOfString StartScheduledTask StartupSound StateDimensions StateFeedbackGains StateOutputEstimator StateResponse StateSpaceModel StateSpaceRealization StateSpaceTransform StationaryDistribution StationaryWaveletPacketTransform StationaryWaveletTransform StatusArea StatusCentrality StepMonitor StieltjesGamma StirlingS1 StirlingS2 StopAsynchronousTask StopScheduledTask StrataVariables StratonovichProcess StreamColorFunction StreamColorFunctionScaling StreamDensityPlot StreamPlot StreamPoints StreamPosition Streams StreamScale StreamStyle String StringBreak StringByteCount StringCases StringCount StringDrop StringExpression StringForm StringFormat StringFreeQ StringInsert StringJoin StringLength StringMatchQ StringPosition StringQ StringReplace StringReplaceList StringReplacePart StringReverse StringRotateLeft StringRotateRight StringSkeleton StringSplit StringTake StringToStream StringTrim StripBoxes StripOnInput StripWrapperBoxes StrokeForm StructuralImportance StructuredArray StructuredSelection StruveH StruveL Stub StudentTDistribution Style StyleBox StyleBoxAutoDelete StyleBoxOptions StyleData StyleDefinitions StyleForm StyleKeyMapping StyleMenuListing StyleNameDialogSettings StyleNames StylePrint StyleSheetPath Subfactorial Subgraph SubMinus SubPlus SubresultantPolynomialRemainders SubresultantPolynomials Subresultants Subscript SubscriptBox SubscriptBoxOptions Subscripted Subset SubsetEqual Subsets SubStar Subsuperscript SubsuperscriptBox SubsuperscriptBoxOptions Subtract SubtractFrom SubValues Succeeds SucceedsEqual SucceedsSlantEqual SucceedsTilde SuchThat Sum SumConvergence Sunday SuperDagger SuperMinus SuperPlus Superscript SuperscriptBox SuperscriptBoxOptions Superset SupersetEqual SuperStar Surd SurdForm SurfaceColor SurfaceGraphics SurvivalDistribution SurvivalFunction SurvivalModel SurvivalModelFit SuspendPacket SuzukiDistribution SuzukiGroupSuz SwatchLegend Switch Symbol SymbolName SymletWavelet Symmetric SymmetricGroup SymmetricMatrixQ SymmetricPolynomial SymmetricReduction Symmetrize SymmetrizedArray SymmetrizedArrayRules SymmetrizedDependentComponents SymmetrizedIndependentComponents SymmetrizedReplacePart SynchronousInitialization SynchronousUpdating Syntax SyntaxForm SyntaxInformation SyntaxLength SyntaxPacket SyntaxQ SystemDialogInput SystemException SystemHelpPath SystemInformation SystemInformationData SystemOpen SystemOptions SystemsModelDelay SystemsModelDelayApproximate SystemsModelDelete SystemsModelDimensions SystemsModelExtract SystemsModelFeedbackConnect SystemsModelLabels SystemsModelOrder SystemsModelParallelConnect SystemsModelSeriesConnect SystemsModelStateFeedbackConnect SystemStub Tab TabFilling Table TableAlignments TableDepth TableDirections TableForm TableHeadings TableSpacing TableView TableViewBox TabSpacings TabView TabViewBox TabViewBoxOptions TagBox TagBoxNote TagBoxOptions TaggingRules TagSet TagSetDelayed TagStyle TagUnset Take TakeWhile Tally Tan Tanh TargetFunctions TargetUnits TautologyQ TelegraphProcess TemplateBox TemplateBoxOptions TemplateSlotSequence TemporalData Temporary TemporaryVariable TensorContract TensorDimensions TensorExpand TensorProduct TensorQ TensorRank TensorReduce TensorSymmetry TensorTranspose TensorWedge Tetrahedron TetrahedronBox TetrahedronBoxOptions TeXForm TeXSave Text Text3DBox Text3DBoxOptions TextAlignment TextBand TextBoundingBox TextBox TextCell TextClipboardType TextData TextForm TextJustification TextLine TextPacket TextParagraph TextRecognize TextRendering TextStyle Texture TextureCoordinateFunction TextureCoordinateScaling Therefore ThermometerGauge Thick Thickness Thin Thinning ThisLink ThompsonGroupTh Thread ThreeJSymbol Threshold Through Throw Thumbnail Thursday Ticks TicksStyle Tilde TildeEqual TildeFullEqual TildeTilde TimeConstrained TimeConstraint Times TimesBy TimeSeriesForecast TimeSeriesInvertibility TimeUsed TimeValue TimeZone Timing Tiny TitleGrouping TitsGroupT ToBoxes ToCharacterCode ToColor ToContinuousTimeModel ToDate ToDiscreteTimeModel ToeplitzMatrix ToExpression ToFileName Together Toggle ToggleFalse Toggler TogglerBar TogglerBox TogglerBoxOptions ToHeldExpression ToInvertibleTimeSeries TokenWords Tolerance ToLowerCase ToNumberField TooBig Tooltip TooltipBox TooltipBoxOptions TooltipDelay TooltipStyle Top TopHatTransform TopologicalSort ToRadicals ToRules ToString Total TotalHeight TotalVariationFilter TotalWidth TouchscreenAutoZoom TouchscreenControlPlacement ToUpperCase Tr Trace TraceAbove TraceAction TraceBackward TraceDepth TraceDialog TraceForward TraceInternal TraceLevel TraceOff TraceOn TraceOriginal TracePrint TraceScan TrackedSymbols TradingChart TraditionalForm TraditionalFunctionNotation TraditionalNotation TraditionalOrder TransferFunctionCancel TransferFunctionExpand TransferFunctionFactor TransferFunctionModel TransferFunctionPoles TransferFunctionTransform TransferFunctionZeros TransformationFunction TransformationFunctions TransformationMatrix TransformedDistribution TransformedField Translate TranslationTransform TransparentColor Transpose TreeForm TreeGraph TreeGraphQ TreePlot TrendStyle TriangleWave TriangularDistribution Trig TrigExpand TrigFactor TrigFactorList Trigger TrigReduce TrigToExp TrimmedMean True TrueQ TruncatedDistribution TsallisQExponentialDistribution TsallisQGaussianDistribution TTest Tube TubeBezierCurveBox TubeBezierCurveBoxOptions TubeBox TubeBSplineCurveBox TubeBSplineCurveBoxOptions Tuesday TukeyLambdaDistribution TukeyWindow Tuples TuranGraph TuringMachine Transparent UnateQ Uncompress Undefined UnderBar Underflow Underlined Underoverscript UnderoverscriptBox UnderoverscriptBoxOptions Underscript UnderscriptBox UnderscriptBoxOptions UndirectedEdge UndirectedGraph UndirectedGraphQ UndocumentedTestFEParserPacket UndocumentedTestGetSelectionPacket Unequal Unevaluated UniformDistribution UniformGraphDistribution UniformSumDistribution Uninstall Union UnionPlus Unique UnitBox UnitConvert UnitDimensions Unitize UnitRootTest UnitSimplify UnitStep UnitTriangle UnitVector Unprotect UnsameQ UnsavedVariables Unset UnsetShared UntrackedVariables Up UpArrow UpArrowBar UpArrowDownArrow Update UpdateDynamicObjects UpdateDynamicObjectsSynchronous UpdateInterval UpDownArrow UpEquilibrium UpperCaseQ UpperLeftArrow UpperRightArrow UpperTriangularize Upsample UpSet UpSetDelayed UpTee UpTeeArrow UpValues URL URLFetch URLFetchAsynchronous URLSave URLSaveAsynchronous UseGraphicsRange Using UsingFrontEnd V2Get ValidationLength Value ValueBox ValueBoxOptions ValueForm ValueQ ValuesData Variables Variance VarianceEquivalenceTest VarianceEstimatorFunction VarianceGammaDistribution VarianceTest VectorAngle VectorColorFunction VectorColorFunctionScaling VectorDensityPlot VectorGlyphData VectorPlot VectorPlot3D VectorPoints VectorQ Vectors VectorScale VectorStyle Vee Verbatim Verbose VerboseConvertToPostScriptPacket VerifyConvergence VerifySolutions VerifyTestAssumptions Version VersionNumber VertexAdd VertexCapacity VertexColors VertexComponent VertexConnectivity VertexCoordinateRules VertexCoordinates VertexCorrelationSimilarity VertexCosineSimilarity VertexCount VertexCoverQ VertexDataCoordinates VertexDegree VertexDelete VertexDiceSimilarity VertexEccentricity VertexInComponent VertexInDegree VertexIndex VertexJaccardSimilarity VertexLabeling VertexLabels VertexLabelStyle VertexList VertexNormals VertexOutComponent VertexOutDegree VertexQ VertexRenderingFunction VertexReplace VertexShape VertexShapeFunction VertexSize VertexStyle VertexTextureCoordinates VertexWeight Vertical VerticalBar VerticalForm VerticalGauge VerticalSeparator VerticalSlider VerticalTilde ViewAngle ViewCenter ViewMatrix ViewPoint ViewPointSelectorSettings ViewPort ViewRange ViewVector ViewVertical VirtualGroupData Visible VisibleCell VoigtDistribution VonMisesDistribution WaitAll WaitAsynchronousTask WaitNext WaitUntil WakebyDistribution WalleniusHypergeometricDistribution WaringYuleDistribution WatershedComponents WatsonUSquareTest WattsStrogatzGraphDistribution WaveletBestBasis WaveletFilterCoefficients WaveletImagePlot WaveletListPlot WaveletMapIndexed WaveletMatrixPlot WaveletPhi WaveletPsi WaveletScale WaveletScalogram WaveletThreshold WeaklyConnectedComponents WeaklyConnectedGraphQ WeakStationarity WeatherData WeberE Wedge Wednesday WeibullDistribution WeierstrassHalfPeriods WeierstrassInvariants WeierstrassP WeierstrassPPrime WeierstrassSigma WeierstrassZeta WeightedAdjacencyGraph WeightedAdjacencyMatrix WeightedData WeightedGraphQ Weights WelchWindow WheelGraph WhenEvent Which While White Whitespace WhitespaceCharacter WhittakerM WhittakerW WienerFilter WienerProcess WignerD WignerSemicircleDistribution WilksW WilksWTest WindowClickSelect WindowElements WindowFloating WindowFrame WindowFrameElements WindowMargins WindowMovable WindowOpacity WindowSelected WindowSize WindowStatusArea WindowTitle WindowToolbars WindowWidth With WolframAlpha WolframAlphaDate WolframAlphaQuantity WolframAlphaResult Word WordBoundary WordCharacter WordData WordSearch WordSeparators WorkingPrecision Write WriteString Wronskian XMLElement XMLObject Xnor Xor Yellow YuleDissimilarity ZernikeR ZeroSymmetric ZeroTest ZeroWidthTimes Zeta ZetaZero ZipfDistribution ZTest ZTransform $Aborted $ActivationGroupID $ActivationKey $ActivationUserRegistered $AddOnsDirectory $AssertFunction $Assumptions $AsynchronousTask $BaseDirectory $BatchInput $BatchOutput $BoxForms $ByteOrdering $Canceled $CharacterEncoding $CharacterEncodings $CommandLine $CompilationTarget $ConditionHold $ConfiguredKernels $Context $ContextPath $ControlActiveSetting $CreationDate $CurrentLink $DateStringFormat $DefaultFont $DefaultFrontEnd $DefaultImagingDevice $DefaultPath $Display $DisplayFunction $DistributedContexts $DynamicEvaluation $Echo $Epilog $ExportFormats $Failed $FinancialDataSource $FormatType $FrontEnd $FrontEndSession $GeoLocation $HistoryLength $HomeDirectory $HTTPCookies $IgnoreEOF $ImagingDevices $ImportFormats $InitialDirectory $Input $InputFileName $InputStreamMethods $Inspector $InstallationDate $InstallationDirectory $InterfaceEnvironment $IterationLimit $KernelCount $KernelID $Language $LaunchDirectory $LibraryPath $LicenseExpirationDate $LicenseID $LicenseProcesses $LicenseServer $LicenseSubprocesses $LicenseType $Line $Linked $LinkSupported $LoadedFiles $MachineAddresses $MachineDomain $MachineDomains $MachineEpsilon $MachineID $MachineName $MachinePrecision $MachineType $MaxExtraPrecision $MaxLicenseProcesses $MaxLicenseSubprocesses $MaxMachineNumber $MaxNumber $MaxPiecewiseCases $MaxPrecision $MaxRootDegree $MessageGroups $MessageList $MessagePrePrint $Messages $MinMachineNumber $MinNumber $MinorReleaseNumber $MinPrecision $ModuleNumber $NetworkLicense $NewMessage $NewSymbol $Notebooks $NumberMarks $Off $OperatingSystem $Output $OutputForms $OutputSizeLimit $OutputStreamMethods $Packages $ParentLink $ParentProcessID $PasswordFile $PatchLevelID $Path $PathnameSeparator $PerformanceGoal $PipeSupported $Post $Pre $PreferencesDirectory $PrePrint $PreRead $PrintForms $PrintLiteral $ProcessID $ProcessorCount $ProcessorType $ProductInformation $ProgramName $RandomState $RecursionLimit $ReleaseNumber $RootDirectory $ScheduledTask $ScriptCommandLine $SessionID $SetParentLink $SharedFunctions $SharedVariables $SoundDisplay $SoundDisplayFunction $SuppressInputFormHeads $SynchronousEvaluation $SyntaxHandler $System $SystemCharacterEncoding $SystemID $SystemWordLength $TemporaryDirectory $TemporaryPrefix $TextStyle $TimedOut $TimeUnit $TimeZone $TopDirectory $TraceOff $TraceOn $TracePattern $TracePostAction $TracePreAction $Urgent $UserAddOnsDirectory $UserBaseDirectory $UserDocumentsDirectory $UserName $Version $VersionNumber",c:[{cN:"comment",b:/\(\*/,e:/\*\)/},a.ASM,a.QSM,a.CNM,{cN:"list",b:/\{/,e:/\}/,i:/:/}]}});hljs.registerLanguage("matlab",function(a){var b=[a.CNM,{cN:"string",b:"'",e:"'",c:[a.BE,{b:"''"}]}];return{k:{keyword:"break case catch classdef continue else elseif end enumerated events for function global if methods otherwise parfor persistent properties return spmd switch try while",built_in:"sin sind sinh asin asind asinh cos cosd cosh acos acosd acosh tan tand tanh atan atand atan2 atanh sec secd sech asec asecd asech csc cscd csch acsc acscd acsch cot cotd coth acot acotd acoth hypot exp expm1 log log1p log10 log2 pow2 realpow reallog realsqrt sqrt nthroot nextpow2 abs angle complex conj imag real unwrap isreal cplxpair fix floor ceil round mod rem sign airy besselj bessely besselh besseli besselk beta betainc betaln ellipj ellipke erf erfc erfcx erfinv expint gamma gammainc gammaln psi legendre cross dot factor isprime primes gcd lcm rat rats perms nchoosek factorial cart2sph cart2pol pol2cart sph2cart hsv2rgb rgb2hsv zeros ones eye repmat rand randn linspace logspace freqspace meshgrid accumarray size length ndims numel disp isempty isequal isequalwithequalnans cat reshape diag blkdiag tril triu fliplr flipud flipdim rot90 find sub2ind ind2sub bsxfun ndgrid permute ipermute shiftdim circshift squeeze isscalar isvector ans eps realmax realmin pi i inf nan isnan isinf isfinite j why compan gallery hadamard hankel hilb invhilb magic pascal rosser toeplitz vander wilkinson"},i:'(//|"|#|/\\*|\\s+/\\w+)',c:[{cN:"function",bK:"function",e:"$",c:[a.UTM,{cN:"params",b:"\\(",e:"\\)"},{cN:"params",b:"\\[",e:"\\]"}]},{cN:"transposed_variable",b:"[a-zA-Z_][a-zA-Z_0-9]*('+[\\.']*|[\\.']+)",e:"",r:0},{cN:"matrix",b:"\\[",e:"\\]'*[\\.']*",c:b,r:0},{cN:"cell",b:"\\{",c:b,i:/:/,v:[{e:/\}'[\.']*/},{e:/\}/,r:0}]},{cN:"comment",b:"\\%",e:"$"}].concat(b)}});hljs.registerLanguage("mel",function(a){return{k:"int float string vector matrix if else switch case default while do for in break continue global proc return about abs addAttr addAttributeEditorNodeHelp addDynamic addNewShelfTab addPP addPanelCategory addPrefixToName advanceToNextDrivenKey affectedNet affects aimConstraint air alias aliasAttr align alignCtx alignCurve alignSurface allViewFit ambientLight angle angleBetween animCone animCurveEditor animDisplay animView annotate appendStringArray applicationName applyAttrPreset applyTake arcLenDimContext arcLengthDimension arclen arrayMapper art3dPaintCtx artAttrCtx artAttrPaintVertexCtx artAttrSkinPaintCtx artAttrTool artBuildPaintMenu artFluidAttrCtx artPuttyCtx artSelectCtx artSetPaintCtx artUserPaintCtx assignCommand assignInputDevice assignViewportFactories attachCurve attachDeviceAttr attachSurface attrColorSliderGrp attrCompatibility attrControlGrp attrEnumOptionMenu attrEnumOptionMenuGrp attrFieldGrp attrFieldSliderGrp attrNavigationControlGrp attrPresetEditWin attributeExists attributeInfo attributeMenu attributeQuery autoKeyframe autoPlace bakeClip bakeFluidShading bakePartialHistory bakeResults bakeSimulation basename basenameEx batchRender bessel bevel bevelPlus binMembership bindSkin blend2 blendShape blendShapeEditor blendShapePanel blendTwoAttr blindDataType boneLattice boundary boxDollyCtx boxZoomCtx bufferCurve buildBookmarkMenu buildKeyframeMenu button buttonManip CBG cacheFile cacheFileCombine cacheFileMerge cacheFileTrack camera cameraView canCreateManip canvas capitalizeString catch catchQuiet ceil changeSubdivComponentDisplayLevel changeSubdivRegion channelBox character characterMap characterOutlineEditor characterize chdir checkBox checkBoxGrp checkDefaultRenderGlobals choice circle circularFillet clamp clear clearCache clip clipEditor clipEditorCurrentTimeCtx clipSchedule clipSchedulerOutliner clipTrimBefore closeCurve closeSurface cluster cmdFileOutput cmdScrollFieldExecuter cmdScrollFieldReporter cmdShell coarsenSubdivSelectionList collision color colorAtPoint colorEditor colorIndex colorIndexSliderGrp colorSliderButtonGrp colorSliderGrp columnLayout commandEcho commandLine commandPort compactHairSystem componentEditor compositingInterop computePolysetVolume condition cone confirmDialog connectAttr connectControl connectDynamic connectJoint connectionInfo constrain constrainValue constructionHistory container containsMultibyte contextInfo control convertFromOldLayers convertIffToPsd convertLightmap convertSolidTx convertTessellation convertUnit copyArray copyFlexor copyKey copySkinWeights cos cpButton cpCache cpClothSet cpCollision cpConstraint cpConvClothToMesh cpForces cpGetSolverAttr cpPanel cpProperty cpRigidCollisionFilter cpSeam cpSetEdit cpSetSolverAttr cpSolver cpSolverTypes cpTool cpUpdateClothUVs createDisplayLayer createDrawCtx createEditor createLayeredPsdFile createMotionField createNewShelf createNode createRenderLayer createSubdivRegion cross crossProduct ctxAbort ctxCompletion ctxEditMode ctxTraverse currentCtx currentTime currentTimeCtx currentUnit curve curveAddPtCtx curveCVCtx curveEPCtx curveEditorCtx curveIntersect curveMoveEPCtx curveOnSurface curveSketchCtx cutKey cycleCheck cylinder dagPose date defaultLightListCheckBox defaultNavigation defineDataServer defineVirtualDevice deformer deg_to_rad delete deleteAttr deleteShadingGroupsAndMaterials deleteShelfTab deleteUI deleteUnusedBrushes delrandstr detachCurve detachDeviceAttr detachSurface deviceEditor devicePanel dgInfo dgdirty dgeval dgtimer dimWhen directKeyCtx directionalLight dirmap dirname disable disconnectAttr disconnectJoint diskCache displacementToPoly displayAffected displayColor displayCull displayLevelOfDetail displayPref displayRGBColor displaySmoothness displayStats displayString displaySurface distanceDimContext distanceDimension doBlur dolly dollyCtx dopeSheetEditor dot dotProduct doubleProfileBirailSurface drag dragAttrContext draggerContext dropoffLocator duplicate duplicateCurve duplicateSurface dynCache dynControl dynExport dynExpression dynGlobals dynPaintEditor dynParticleCtx dynPref dynRelEdPanel dynRelEditor dynamicLoad editAttrLimits editDisplayLayerGlobals editDisplayLayerMembers editRenderLayerAdjustment editRenderLayerGlobals editRenderLayerMembers editor editorTemplate effector emit emitter enableDevice encodeString endString endsWith env equivalent equivalentTol erf error eval evalDeferred evalEcho event exactWorldBoundingBox exclusiveLightCheckBox exec executeForEachObject exists exp expression expressionEditorListen extendCurve extendSurface extrude fcheck fclose feof fflush fgetline fgetword file fileBrowserDialog fileDialog fileExtension fileInfo filetest filletCurve filter filterCurve filterExpand filterStudioImport findAllIntersections findAnimCurves findKeyframe findMenuItem findRelatedSkinCluster finder firstParentOf fitBspline flexor floatEq floatField floatFieldGrp floatScrollBar floatSlider floatSlider2 floatSliderButtonGrp floatSliderGrp floor flow fluidCacheInfo fluidEmitter fluidVoxelInfo flushUndo fmod fontDialog fopen formLayout format fprint frameLayout fread freeFormFillet frewind fromNativePath fwrite gamma gauss geometryConstraint getApplicationVersionAsFloat getAttr getClassification getDefaultBrush getFileList getFluidAttr getInputDeviceRange getMayaPanelTypes getModifiers getPanel getParticleAttr getPluginResource getenv getpid glRender glRenderEditor globalStitch gmatch goal gotoBindPose grabColor gradientControl gradientControlNoAttr graphDollyCtx graphSelectContext graphTrackCtx gravity grid gridLayout group groupObjectsByName HfAddAttractorToAS HfAssignAS HfBuildEqualMap HfBuildFurFiles HfBuildFurImages HfCancelAFR HfConnectASToHF HfCreateAttractor HfDeleteAS HfEditAS HfPerformCreateAS HfRemoveAttractorFromAS HfSelectAttached HfSelectAttractors HfUnAssignAS hardenPointCurve hardware hardwareRenderPanel headsUpDisplay headsUpMessage help helpLine hermite hide hilite hitTest hotBox hotkey hotkeyCheck hsv_to_rgb hudButton hudSlider hudSliderButton hwReflectionMap hwRender hwRenderLoad hyperGraph hyperPanel hyperShade hypot iconTextButton iconTextCheckBox iconTextRadioButton iconTextRadioCollection iconTextScrollList iconTextStaticLabel ikHandle ikHandleCtx ikHandleDisplayScale ikSolver ikSplineHandleCtx ikSystem ikSystemInfo ikfkDisplayMethod illustratorCurves image imfPlugins inheritTransform insertJoint insertJointCtx insertKeyCtx insertKnotCurve insertKnotSurface instance instanceable instancer intField intFieldGrp intScrollBar intSlider intSliderGrp interToUI internalVar intersect iprEngine isAnimCurve isConnected isDirty isParentOf isSameObject isTrue isValidObjectName isValidString isValidUiName isolateSelect itemFilter itemFilterAttr itemFilterRender itemFilterType joint jointCluster jointCtx jointDisplayScale jointLattice keyTangent keyframe keyframeOutliner keyframeRegionCurrentTimeCtx keyframeRegionDirectKeyCtx keyframeRegionDollyCtx keyframeRegionInsertKeyCtx keyframeRegionMoveKeyCtx keyframeRegionScaleKeyCtx keyframeRegionSelectKeyCtx keyframeRegionSetKeyCtx keyframeRegionTrackCtx keyframeStats lassoContext lattice latticeDeformKeyCtx launch launchImageEditor layerButton layeredShaderPort layeredTexturePort layout layoutDialog lightList lightListEditor lightListPanel lightlink lineIntersection linearPrecision linstep listAnimatable listAttr listCameras listConnections listDeviceAttachments listHistory listInputDeviceAxes listInputDeviceButtons listInputDevices listMenuAnnotation listNodeTypes listPanelCategories listRelatives listSets listTransforms listUnselected listerEditor loadFluid loadNewShelf loadPlugin loadPluginLanguageResources loadPrefObjects localizedPanelLabel lockNode loft log longNameOf lookThru ls lsThroughFilter lsType lsUI Mayatomr mag makeIdentity makeLive makePaintable makeRoll makeSingleSurface makeTubeOn makebot manipMoveContext manipMoveLimitsCtx manipOptions manipRotateContext manipRotateLimitsCtx manipScaleContext manipScaleLimitsCtx marker match max memory menu menuBarLayout menuEditor menuItem menuItemToShelf menuSet menuSetPref messageLine min minimizeApp mirrorJoint modelCurrentTimeCtx modelEditor modelPanel mouse movIn movOut move moveIKtoFK moveKeyCtx moveVertexAlongDirection multiProfileBirailSurface mute nParticle nameCommand nameField namespace namespaceInfo newPanelItems newton nodeCast nodeIconButton nodeOutliner nodePreset nodeType noise nonLinear normalConstraint normalize nurbsBoolean nurbsCopyUVSet nurbsCube nurbsEditUV nurbsPlane nurbsSelect nurbsSquare nurbsToPoly nurbsToPolygonsPref nurbsToSubdiv nurbsToSubdivPref nurbsUVSet nurbsViewDirectionVector objExists objectCenter objectLayer objectType objectTypeUI obsoleteProc oceanNurbsPreviewPlane offsetCurve offsetCurveOnSurface offsetSurface openGLExtension openMayaPref optionMenu optionMenuGrp optionVar orbit orbitCtx orientConstraint outlinerEditor outlinerPanel overrideModifier paintEffectsDisplay pairBlend palettePort paneLayout panel panelConfiguration panelHistory paramDimContext paramDimension paramLocator parent parentConstraint particle particleExists particleInstancer particleRenderInfo partition pasteKey pathAnimation pause pclose percent performanceOptions pfxstrokes pickWalk picture pixelMove planarSrf plane play playbackOptions playblast plugAttr plugNode pluginInfo pluginResourceUtil pointConstraint pointCurveConstraint pointLight pointMatrixMult pointOnCurve pointOnSurface pointPosition poleVectorConstraint polyAppend polyAppendFacetCtx polyAppendVertex polyAutoProjection polyAverageNormal polyAverageVertex polyBevel polyBlendColor polyBlindData polyBoolOp polyBridgeEdge polyCacheMonitor polyCheck polyChipOff polyClipboard polyCloseBorder polyCollapseEdge polyCollapseFacet polyColorBlindData polyColorDel polyColorPerVertex polyColorSet polyCompare polyCone polyCopyUV polyCrease polyCreaseCtx polyCreateFacet polyCreateFacetCtx polyCube polyCut polyCutCtx polyCylinder polyCylindricalProjection polyDelEdge polyDelFacet polyDelVertex polyDuplicateAndConnect polyDuplicateEdge polyEditUV polyEditUVShell polyEvaluate polyExtrudeEdge polyExtrudeFacet polyExtrudeVertex polyFlipEdge polyFlipUV polyForceUV polyGeoSampler polyHelix polyInfo polyInstallAction polyLayoutUV polyListComponentConversion polyMapCut polyMapDel polyMapSew polyMapSewMove polyMergeEdge polyMergeEdgeCtx polyMergeFacet polyMergeFacetCtx polyMergeUV polyMergeVertex polyMirrorFace polyMoveEdge polyMoveFacet polyMoveFacetUV polyMoveUV polyMoveVertex polyNormal polyNormalPerVertex polyNormalizeUV polyOptUvs polyOptions polyOutput polyPipe polyPlanarProjection polyPlane polyPlatonicSolid polyPoke polyPrimitive polyPrism polyProjection polyPyramid polyQuad polyQueryBlindData polyReduce polySelect polySelectConstraint polySelectConstraintMonitor polySelectCtx polySelectEditCtx polySeparate polySetToFaceNormal polySewEdge polyShortestPathCtx polySmooth polySoftEdge polySphere polySphericalProjection polySplit polySplitCtx polySplitEdge polySplitRing polySplitVertex polyStraightenUVBorder polySubdivideEdge polySubdivideFacet polyToSubdiv polyTorus polyTransfer polyTriangulate polyUVSet polyUnite polyWedgeFace popen popupMenu pose pow preloadRefEd print progressBar progressWindow projFileViewer projectCurve projectTangent projectionContext projectionManip promptDialog propModCtx propMove psdChannelOutliner psdEditTextureFile psdExport psdTextureFile putenv pwd python querySubdiv quit rad_to_deg radial radioButton radioButtonGrp radioCollection radioMenuItemCollection rampColorPort rand randomizeFollicles randstate rangeControl readTake rebuildCurve rebuildSurface recordAttr recordDevice redo reference referenceEdit referenceQuery refineSubdivSelectionList refresh refreshAE registerPluginResource rehash reloadImage removeJoint removeMultiInstance removePanelCategory rename renameAttr renameSelectionList renameUI render renderGlobalsNode renderInfo renderLayerButton renderLayerParent renderLayerPostProcess renderLayerUnparent renderManip renderPartition renderQualityNode renderSettings renderThumbnailUpdate renderWindowEditor renderWindowSelectContext renderer reorder reorderDeformers requires reroot resampleFluid resetAE resetPfxToPolyCamera resetTool resolutionNode retarget reverseCurve reverseSurface revolve rgb_to_hsv rigidBody rigidSolver roll rollCtx rootOf rot rotate rotationInterpolation roundConstantRadius rowColumnLayout rowLayout runTimeCommand runup sampleImage saveAllShelves saveAttrPreset saveFluid saveImage saveInitialState saveMenu savePrefObjects savePrefs saveShelf saveToolSettings scale scaleBrushBrightness scaleComponents scaleConstraint scaleKey scaleKeyCtx sceneEditor sceneUIReplacement scmh scriptCtx scriptEditorInfo scriptJob scriptNode scriptTable scriptToShelf scriptedPanel scriptedPanelType scrollField scrollLayout sculpt searchPathArray seed selLoadSettings select selectContext selectCurveCV selectKey selectKeyCtx selectKeyframeRegionCtx selectMode selectPref selectPriority selectType selectedNodes selectionConnection separator setAttr setAttrEnumResource setAttrMapping setAttrNiceNameResource setConstraintRestPosition setDefaultShadingGroup setDrivenKeyframe setDynamic setEditCtx setEditor setFluidAttr setFocus setInfinity setInputDeviceMapping setKeyCtx setKeyPath setKeyframe setKeyframeBlendshapeTargetWts setMenuMode setNodeNiceNameResource setNodeTypeFlag setParent setParticleAttr setPfxToPolyCamera setPluginResource setProject setStampDensity setStartupMessage setState setToolTo setUITemplate setXformManip sets shadingConnection shadingGeometryRelCtx shadingLightRelCtx shadingNetworkCompare shadingNode shapeCompare shelfButton shelfLayout shelfTabLayout shellField shortNameOf showHelp showHidden showManipCtx showSelectionInTitle showShadingGroupAttrEditor showWindow sign simplify sin singleProfileBirailSurface size sizeBytes skinCluster skinPercent smoothCurve smoothTangentSurface smoothstep snap2to2 snapKey snapMode snapTogetherCtx snapshot soft softMod softModCtx sort sound soundControl source spaceLocator sphere sphrand spotLight spotLightPreviewPort spreadSheetEditor spring sqrt squareSurface srtContext stackTrace startString startsWith stitchAndExplodeShell stitchSurface stitchSurfacePoints strcmp stringArrayCatenate stringArrayContains stringArrayCount stringArrayInsertAtIndex stringArrayIntersector stringArrayRemove stringArrayRemoveAtIndex stringArrayRemoveDuplicates stringArrayRemoveExact stringArrayToString stringToStringArray strip stripPrefixFromName stroke subdAutoProjection subdCleanTopology subdCollapse subdDuplicateAndConnect subdEditUV subdListComponentConversion subdMapCut subdMapSewMove subdMatchTopology subdMirror subdToBlind subdToPoly subdTransferUVsToCache subdiv subdivCrease subdivDisplaySmoothness substitute substituteAllString substituteGeometry substring surface surfaceSampler surfaceShaderList swatchDisplayPort switchTable symbolButton symbolCheckBox sysFile system tabLayout tan tangentConstraint texLatticeDeformContext texManipContext texMoveContext texMoveUVShellContext texRotateContext texScaleContext texSelectContext texSelectShortestPathCtx texSmudgeUVContext texWinToolCtx text textCurves textField textFieldButtonGrp textFieldGrp textManip textScrollList textToShelf textureDisplacePlane textureHairColor texturePlacementContext textureWindow threadCount threePointArcCtx timeControl timePort timerX toNativePath toggle toggleAxis toggleWindowVisibility tokenize tokenizeList tolerance tolower toolButton toolCollection toolDropped toolHasOptions toolPropertyWindow torus toupper trace track trackCtx transferAttributes transformCompare transformLimits translator trim trunc truncateFluidCache truncateHairCache tumble tumbleCtx turbulence twoPointArcCtx uiRes uiTemplate unassignInputDevice undo undoInfo ungroup uniform unit unloadPlugin untangleUV untitledFileName untrim upAxis updateAE userCtx uvLink uvSnapshot validateShelfName vectorize view2dToolCtx viewCamera viewClipPlane viewFit viewHeadOn viewLookAt viewManip viewPlace viewSet visor volumeAxis vortex waitCursor warning webBrowser webBrowserPrefs whatIs window windowPref wire wireContext workspace wrinkle wrinkleContext writeTake xbmLangPathList xform",i:"</",c:[a.CNM,a.ASM,a.QSM,{cN:"string",b:"`",e:"`",c:[a.BE]},{cN:"variable",v:[{b:"\\$\\d"},{b:"[\\$\\%\\@](\\^\\w\\b|#\\w+|[^\\s\\w{]|{\\w+}|\\w+)"},{b:"\\*(\\^\\w\\b|#\\w+|[^\\s\\w{]|{\\w+}|\\w+)",r:0}]},a.CLCM,a.CBCM]}});hljs.registerLanguage("mizar",function(a){return{k:["environ vocabularies notations constructors definitions registrations theorems schemes requirements","begin end definition registration cluster existence pred func defpred deffunc theorem proof","let take assume then thus hence ex for st holds consider reconsider such that and in provided of as from","be being by means equals implies iff redefine define now not or attr is mode suppose per cases set","thesis contradiction scheme reserve struct","correctness compatibility coherence symmetry assymetry reflexivity irreflexivity","connectedness uniqueness commutativity idempotence involutiveness projectivity"].join(" "),c:[{cN:"comment",b:"::",e:"$"}]}});hljs.registerLanguage("monkey",function(a){var b={v:[{cN:"number",b:"[$][a-fA-F0-9]+"},a.NM]};return{cI:true,k:{keyword:"public private property continue exit extern new try catch eachin not abstract final select case default const local global field end if then else elseif endif while wend repeat until forever for to step next return module inline throw",built_in:"DebugLog DebugStop Error Print ACos ACosr ASin ASinr ATan ATan2 ATan2r ATanr Abs Abs Ceil Clamp Clamp Cos Cosr Exp Floor Log Max Max Min Min Pow Sgn Sgn Sin Sinr Sqrt Tan Tanr Seed PI HALFPI TWOPI",literal:"true false null and or shl shr mod"},c:[{cN:"comment",b:"#rem",e:"#end"},{cN:"comment",b:"'",e:"$",r:0},{cN:"function",bK:"function method",e:"[(=:]|$",i:/\n/,c:[a.UTM,]},{cN:"class",bK:"class interface",e:"$",c:[{bK:"extends implements"},a.UTM]},{cN:"variable",b:"\\b(self|super)\\b"},{cN:"preprocessor",bK:"import",e:"$"},{cN:"preprocessor",b:"\\s*#",e:"$",k:"if else elseif endif end then"},{cN:"pi",b:"^\\s*strict\\b"},{bK:"alias",e:"=",c:[a.UTM]},a.QSM,b]}});hljs.registerLanguage("nginx",function(c){var b={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+c.UIR}]};var a={eW:true,l:"[a-z/_]+",k:{built_in:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[c.HCM,{cN:"string",c:[c.BE,b],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{cN:"url",b:"([a-z]+):/",e:"\\s",eW:true,eE:true,c:[b]},{cN:"regexp",c:[c.BE,b],v:[{b:"\\s\\^",e:"\\s|{|;",rE:true},{b:"~\\*?\\s+",e:"\\s|{|;",rE:true},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},b]};return{aliases:["nginxconf"],c:[c.HCM,{b:c.UIR+"\\s",e:";|{",rB:true,c:[{cN:"title",b:c.UIR,starts:a}],r:0}],i:"[^\\s\\}]"}});hljs.registerLanguage("nimrod",function(a){return{k:{keyword:"addr and as asm bind block break|0 case|0 cast const|0 continue|0 converter discard distinct|10 div do elif else|0 end|0 enum|0 except export finally for from generic if|0 import|0 in include|0 interface is isnot|10 iterator|10 let|0 macro method|10 mixin mod nil not notin|10 object|0 of or out proc|10 ptr raise ref|10 return shl shr static template|10 try|0 tuple type|0 using|0 var|0 when while|0 with without xor yield",literal:"shared guarded stdin stdout stderr result|10 true false"},c:[{cN:"decorator",b:/{\./,e:/\.}/,r:10},{cN:"string",b:/[a-zA-Z]\w*"/,e:/"/,c:[{b:/""/}]},{cN:"string",b:/([a-zA-Z]\w*)?"""/,e:/"""/},{cN:"string",b:/"/,e:/"/,i:/\n/,c:[{b:/\\./}]},{cN:"type",b:/\b[A-Z]\w+\b/,r:0},{cN:"type",b:/\b(int|int8|int16|int32|int64|uint|uint8|uint16|uint32|uint64|float|float32|float64|bool|char|string|cstring|pointer|expr|stmt|void|auto|any|range|array|openarray|varargs|seq|set|clong|culong|cchar|cschar|cshort|cint|csize|clonglong|cfloat|cdouble|clongdouble|cuchar|cushort|cuint|culonglong|cstringarray|semistatic)\b/},{cN:"number",b:/\b(0[xX][0-9a-fA-F][_0-9a-fA-F]*)('?[iIuU](8|16|32|64))?/,r:0},{cN:"number",b:/\b(0o[0-7][_0-7]*)('?[iIuUfF](8|16|32|64))?/,r:0},{cN:"number",b:/\b(0(b|B)[01][_01]*)('?[iIuUfF](8|16|32|64))?/,r:0},{cN:"number",b:/\b(\d[_\d]*)('?[iIuUfF](8|16|32|64))?/,r:0},a.HCM]}});hljs.registerLanguage("nix",function(b){var a={keyword:"rec with let in inherit assert if else then",constant:"true false or and null",built_in:"import abort baseNameOf dirOf isNull builtins map removeAttrs throw toString derivation"};var g={cN:"subst",b:/\$\{/,e:/\}/,k:a};var d={cN:"variable",b:/[a-zA-Z0-9-_]+(\s*=)/};var e={cN:"string",b:"''",e:"''",c:[g]};var f={cN:"string",b:'"',e:'"',c:[g]};var c=[b.NM,b.HCM,b.CBCM,e,f,d];g.c=c;return{aliases:["nixos"],k:a,c:c}});hljs.registerLanguage("nsis",function(a){var c={cN:"symbol",b:"\\$(ADMINTOOLS|APPDATA|CDBURN_AREA|CMDLINE|COMMONFILES32|COMMONFILES64|COMMONFILES|COOKIES|DESKTOP|DOCUMENTS|EXEDIR|EXEFILE|EXEPATH|FAVORITES|FONTS|HISTORY|HWNDPARENT|INSTDIR|INTERNET_CACHE|LANGUAGE|LOCALAPPDATA|MUSIC|NETHOOD|OUTDIR|PICTURES|PLUGINSDIR|PRINTHOOD|PROFILE|PROGRAMFILES32|PROGRAMFILES64|PROGRAMFILES|QUICKLAUNCH|RECENT|RESOURCES_LOCALIZED|RESOURCES|SENDTO|SMPROGRAMS|SMSTARTUP|STARTMENU|SYSDIR|TEMP|TEMPLATES|VIDEOS|WINDIR)"};var b={cN:"constant",b:"\\$+{[a-zA-Z0-9_]+}"};var f={cN:"variable",b:"\\$+[a-zA-Z0-9_]+",i:"\\(\\){}"};var e={cN:"constant",b:"\\$+\\([a-zA-Z0-9_]+\\)"};var g={cN:"params",b:"(ARCHIVE|FILE_ATTRIBUTE_ARCHIVE|FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_OFFLINE|FILE_ATTRIBUTE_READONLY|FILE_ATTRIBUTE_SYSTEM|FILE_ATTRIBUTE_TEMPORARY|HKCR|HKCU|HKDD|HKEY_CLASSES_ROOT|HKEY_CURRENT_CONFIG|HKEY_CURRENT_USER|HKEY_DYN_DATA|HKEY_LOCAL_MACHINE|HKEY_PERFORMANCE_DATA|HKEY_USERS|HKLM|HKPD|HKU|IDABORT|IDCANCEL|IDIGNORE|IDNO|IDOK|IDRETRY|IDYES|MB_ABORTRETRYIGNORE|MB_DEFBUTTON1|MB_DEFBUTTON2|MB_DEFBUTTON3|MB_DEFBUTTON4|MB_ICONEXCLAMATION|MB_ICONINFORMATION|MB_ICONQUESTION|MB_ICONSTOP|MB_OK|MB_OKCANCEL|MB_RETRYCANCEL|MB_RIGHT|MB_RTLREADING|MB_SETFOREGROUND|MB_TOPMOST|MB_USERICON|MB_YESNO|NORMAL|OFFLINE|READONLY|SHCTX|SHELL_CONTEXT|SYSTEM|TEMPORARY)"};var d={cN:"constant",b:"\\!(addincludedir|addplugindir|appendfile|cd|define|delfile|echo|else|endif|error|execute|finalize|getdllversionsystem|ifdef|ifmacrodef|ifmacrondef|ifndef|if|include|insertmacro|macroend|macro|packhdr|searchparse|searchreplace|tempfile|undef|verbose|warning)"};return{cI:false,k:{keyword:"Abort AddBrandingImage AddSize AllowRootDirInstall AllowSkipFiles AutoCloseWindow BGFont BGGradient BrandingText BringToFront Call CallInstDLL Caption ChangeUI CheckBitmap ClearErrors CompletedText ComponentText CopyFiles CRCCheck CreateDirectory CreateFont CreateShortCut Delete DeleteINISec DeleteINIStr DeleteRegKey DeleteRegValue DetailPrint DetailsButtonText DirText DirVar DirVerify EnableWindow EnumRegKey EnumRegValue Exch Exec ExecShell ExecWait ExpandEnvStrings File FileBufSize FileClose FileErrorText FileOpen FileRead FileReadByte FileReadUTF16LE FileReadWord FileSeek FileWrite FileWriteByte FileWriteUTF16LE FileWriteWord FindClose FindFirst FindNext FindWindow FlushINI FunctionEnd GetCurInstType GetCurrentAddress GetDlgItem GetDLLVersion GetDLLVersionLocal GetErrorLevel GetFileTime GetFileTimeLocal GetFullPathName GetFunctionAddress GetInstDirError GetLabelAddress GetTempFileName Goto HideWindow Icon IfAbort IfErrors IfFileExists IfRebootFlag IfSilent InitPluginsDir InstallButtonText InstallColors InstallDir InstallDirRegKey InstProgressFlags InstType InstTypeGetText InstTypeSetText IntCmp IntCmpU IntFmt IntOp IsWindow LangString LicenseBkColor LicenseData LicenseForceSelection LicenseLangString LicenseText LoadLanguageFile LockWindow LogSet LogText ManifestDPIAware ManifestSupportedOS MessageBox MiscButtonText Name Nop OutFile Page PageCallbacks PageExEnd Pop Push Quit ReadEnvStr ReadINIStr ReadRegDWORD ReadRegStr Reboot RegDLL Rename RequestExecutionLevel ReserveFile Return RMDir SearchPath SectionEnd SectionGetFlags SectionGetInstTypes SectionGetSize SectionGetText SectionGroupEnd SectionIn SectionSetFlags SectionSetInstTypes SectionSetSize SectionSetText SendMessage SetAutoClose SetBrandingImage SetCompress SetCompressor SetCompressorDictSize SetCtlColors SetCurInstType SetDatablockOptimize SetDateSave SetDetailsPrint SetDetailsView SetErrorLevel SetErrors SetFileAttributes SetFont SetOutPath SetOverwrite SetPluginUnload SetRebootFlag SetRegView SetShellVarContext SetSilent ShowInstDetails ShowUninstDetails ShowWindow SilentInstall SilentUnInstall Sleep SpaceTexts StrCmp StrCmpS StrCpy StrLen SubCaption SubSectionEnd Unicode UninstallButtonText UninstallCaption UninstallIcon UninstallSubCaption UninstallText UninstPage UnRegDLL Var VIAddVersionKey VIFileVersion VIProductVersion WindowIcon WriteINIStr WriteRegBin WriteRegDWORD WriteRegExpandStr WriteRegStr WriteUninstaller XPStyle",literal:"admin all auto both colored current false force hide highest lastused leave listonly none normal notset off on open print show silent silentlog smooth textonly true user "},c:[a.HCM,a.CBCM,{cN:"string",b:'"',e:'"',i:"\\n",c:[{cN:"symbol",b:"\\$(\\\\(n|r|t)|\\$)"},c,b,f,e]},{cN:"comment",b:";",e:"$",r:0},{cN:"function",bK:"Function PageEx Section SectionGroup SubSection",e:"$"},d,b,f,e,g,a.NM,{cN:"literal",b:a.IR+"::"+a.IR}]}});hljs.registerLanguage("objectivec",function(a){var d={keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"NSString NSData NSDictionary CGRect CGPoint UIButton UILabel UITextView UIWebView MKMapView NSView NSViewController NSWindow NSWindowController NSSet NSUUID NSIndexSet UISegmentedControl NSObject UITableViewDelegate UITableViewDataSource NSThread UIActivityIndicator UITabbar UIToolBar UIBarButtonItem UIImageView NSAutoreleasePool UITableView BOOL NSInteger CGFloat NSException NSLog NSMutableString NSMutableArray NSMutableDictionary NSURL NSIndexPath CGSize UITableViewCell UIView UIViewController UINavigationBar UINavigationController UITabBarController UIPopoverController UIPopoverControllerDelegate UIImage NSNumber UISearchBar NSFetchedResultsController NSFetchedResultsChangeType UIScrollView UIScrollViewDelegate UIEdgeInsets UIColor UIFont UIApplication NSNotFound NSNotificationCenter NSNotification UILocalNotification NSBundle NSFileManager NSTimeInterval NSDate NSCalendar NSUserDefaults UIWindow NSRange NSArray NSError NSURLRequest NSURLConnection NSURLSession NSURLSessionDataTask NSURLSessionDownloadTask NSURLSessionUploadTask NSURLResponseUIInterfaceOrientation MPMoviePlayerController dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"};var c=/[a-zA-Z@][a-zA-Z0-9_]*/;var b="@interface @class @protocol @implementation";return{aliases:["m","mm","objc","obj-c"],k:d,l:c,i:"</",c:[a.CLCM,a.CBCM,a.CNM,a.QSM,{cN:"string",v:[{b:'@"',e:'"',i:"\\n",c:[a.BE]},{b:"'",e:"[^\\\\]'",i:"[^\\\\][^']"}]},{cN:"preprocessor",b:"#",e:"$",c:[{cN:"title",v:[{b:'"',e:'"'},{b:"<",e:">"}]}]},{cN:"class",b:"("+b.split(" ").join("|")+")\\b",e:"({|$)",eE:true,k:b,l:c,c:[a.UTM]},{cN:"variable",b:"\\."+a.UIR,r:0}]}});hljs.registerLanguage("ocaml",function(a){return{aliases:["ml"],k:{keyword:"and as assert asr begin class constraint do done downto else end exception external false for fun function functor if in include inherit initializer land lazy let lor lsl lsr lxor match method mod module mutable new object of open or private rec ref sig struct then to true try type val virtual when while with parser value",built_in:"bool char float int list unit array exn option int32 int64 nativeint format4 format6 lazy_t in_channel out_channel string"},i:/\/\//,c:[{cN:"string",b:'"""',e:'"""'},{cN:"comment",b:"\\(\\*",e:"\\*\\)",c:["self"]},{cN:"class",bK:"type",e:"\\(|=|$",eE:true,c:[a.UTM]},{cN:"annotation",b:"\\[<",e:">\\]"},a.CBCM,a.inherit(a.ASM,{i:null}),a.inherit(a.QSM,{i:null}),a.CNM]}});hljs.registerLanguage("oxygene",function(b){var g="abstract add and array as asc aspect assembly async begin break block by case class concat const copy constructor continue create default delegate desc distinct div do downto dynamic each else empty end ensure enum equals event except exit extension external false final finalize finalizer finally flags for forward from function future global group has if implementation implements implies in index inherited inline interface into invariants is iterator join locked locking loop matching method mod module namespace nested new nil not notify nullable of old on operator or order out override parallel params partial pinned private procedure property protected public queryable raise read readonly record reintroduce remove repeat require result reverse sealed select self sequence set shl shr skip static step soft take then to true try tuple type union unit unsafe until uses using var virtual raises volatile where while with write xor yield await mapped deprecated stdcall cdecl pascal register safecall overload library platform reference packed strict published autoreleasepool selector strong weak unretained";var a={cN:"comment",b:"{",e:"}",r:0};var e={cN:"comment",b:"\\(\\*",e:"\\*\\)",r:10};var c={cN:"string",b:"'",e:"'",c:[{b:"''"}]};var d={cN:"string",b:"(#\\d+)+"};var f={cN:"function",bK:"function constructor destructor procedure method",e:"[:;]",k:"function constructor|10 destructor|10 procedure|10 method|10",c:[b.TM,{cN:"params",b:"\\(",e:"\\)",k:g,c:[c,d]},a,e]};return{cI:true,k:g,i:'("|\\$[G-Zg-z]|\\/\\*|</)',c:[a,e,b.CLCM,c,d,b.NM,f,{cN:"class",b:"=\\bclass\\b",e:"end;",k:g,c:[c,d,a,e,b.CLCM,f]}]}});hljs.registerLanguage("parser3",function(a){return{sL:"xml",r:0,c:[{cN:"comment",b:"^#",e:"$"},{cN:"comment",b:"\\^rem{",e:"}",r:10,c:[{b:"{",e:"}",c:["self"]}]},{cN:"preprocessor",b:"^@(?:BASE|USE|CLASS|OPTIONS)$",r:10},{cN:"title",b:"@[\\w\\-]+\\[[\\w^;\\-]*\\](?:\\[[\\w^;\\-]*\\])?(?:.*)$"},{cN:"variable",b:"\\$\\{?[\\w\\-\\.\\:]+\\}?"},{cN:"keyword",b:"\\^[\\w\\-\\.\\:]+"},{cN:"number",b:"\\^#[0-9a-fA-F]+"},a.CNM]}});hljs.registerLanguage("perl",function(c){var d="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when";var f={cN:"subst",b:"[$@]\\{",e:"\\}",k:d};var g={b:"->{",e:"}"};var a={cN:"variable",v:[{b:/\$\d/},{b:/[\$\%\@](\^\w\b|#\w+(\:\:\w+)*|{\w+}|\w+(\:\:\w*)*)/},{b:/[\$\%\@][^\s\w{]/,r:0}]};var e={cN:"comment",b:"^(__END__|__DATA__)",e:"\\n$",r:5};var h=[c.BE,f,a];var b=[a,c.HCM,e,{cN:"comment",b:"^\\=\\w",e:"\\=cut",eW:true},g,{cN:"string",c:h,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[c.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[c.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+c.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[c.HCM,e,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[c.BE],r:0}]},{cN:"sub",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",r:5},{cN:"operator",b:"-\\w\\b",r:0}];f.c=b;g.c=b;return{aliases:["pl"],k:d,c:b}});hljs.registerLanguage("php",function(b){var e={cN:"variable",b:"(\\$|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*"};var a={cN:"preprocessor",b:/<\?(php)?|\?>/};var c={cN:"string",c:[b.BE,a],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},b.inherit(b.ASM,{i:null}),b.inherit(b.QSM,{i:null})]};var d={v:[b.BNM,b.CNM]};return{aliases:["php3","php4","php5","php6"],cI:true,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[b.CLCM,b.HCM,{cN:"comment",b:"/\\*",e:"\\*/",c:[{cN:"phpdoc",b:"\\s@[A-Za-z]+"},a]},{cN:"comment",b:"__halt_compiler.+?;",eW:true,k:"__halt_compiler",l:b.UIR},{cN:"string",b:"<<<['\"]?\\w+['\"]?$",e:"^\\w+;",c:[b.BE]},a,e,{cN:"function",bK:"function",e:/[;{]/,eE:true,i:"\\$|\\[|%",c:[b.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",e,b.CBCM,c,d]}]},{cN:"class",bK:"class interface",e:"{",eE:true,i:/[:\(\$"]/,c:[{bK:"extends implements"},b.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[b.UTM]},{bK:"use",e:";",c:[b.UTM]},{b:"=>"},c,d]}});hljs.registerLanguage("profile",function(a){return{c:[a.CNM,{cN:"built_in",b:"{",e:"}$",eB:true,eE:true,c:[a.ASM,a.QSM],r:0},{cN:"filename",b:"[a-zA-Z_][\\da-zA-Z_]+\\.[\\da-zA-Z_]{1,3}",e:":",eE:true},{cN:"header",b:"(ncalls|tottime|cumtime)",e:"$",k:"ncalls tottime|10 cumtime|10 filename",r:10},{cN:"summary",b:"function calls",e:"$",c:[a.CNM],r:10},a.ASM,a.QSM,{cN:"function",b:"\\(",e:"\\)$",c:[a.UTM],r:0}]}});hljs.registerLanguage("protobuf",function(a){return{k:{keyword:"package import option optional required repeated group",built_in:"double float int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string bytes",literal:"true false"},c:[a.QSM,a.NM,a.CLCM,{cN:"class",bK:"message enum service",e:/\{/,i:/\n/,c:[a.inherit(a.TM,{starts:{eW:true,eE:true}})]},{cN:"function",bK:"rpc",e:/;/,eE:true,k:"rpc returns"},{cN:"constant",b:/^\s*[A-Z_]+/,e:/\s*=/,eE:true}]}});hljs.registerLanguage("python",function(a){var f={cN:"prompt",b:/^(>>>|\.\.\.) /};var b={cN:"string",c:[a.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[f],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[f],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},a.ASM,a.QSM]};var d={cN:"number",r:0,v:[{b:a.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:a.CNR+"[lLjJ]?"}]};var e={cN:"params",b:/\(/,e:/\)/,c:["self",f,d,b]};var c={e:/:/,i:/[${=;\n]/,c:[a.UTM,e]};return{aliases:["py","gyp"],k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)/,c:[f,d,b,a.HCM,a.inherit(c,{cN:"function",bK:"def",r:10}),a.inherit(c,{cN:"class",bK:"class"}),{cN:"decorator",b:/@/,e:/$/},{b:/\b(print|exec)\(/}]}});hljs.registerLanguage("q",function(a){var b={keyword:"do while select delete by update from",constant:"0b 1b",built_in:"neg not null string reciprocal floor ceiling signum mod xbar xlog and or each scan over prior mmu lsq inv md5 ltime gtime count first var dev med cov cor all any rand sums prds mins maxs fills deltas ratios avgs differ prev next rank reverse iasc idesc asc desc msum mcount mavg mdev xrank mmin mmax xprev rotate distinct group where flip type key til get value attr cut set upsert raze union inter except cross sv vs sublist enlist read0 read1 hopen hclose hdel hsym hcount peach system ltrim rtrim trim lower upper ssr view tables views cols xcols keys xkey xcol xasc xdesc fkeys meta lj aj aj0 ij pj asof uj ww wj wj1 fby xgroup ungroup ej save load rsave rload show csv parse eval min max avg wavg wsum sin cos tan sum",typename:"`float `double int `timestamp `timespan `datetime `time `boolean `symbol `char `byte `short `long `real `month `date `minute `second `guid"};return{aliases:["k","kdb"],k:b,l:/\b(`?)[A-Za-z0-9_]+\b/,c:[a.CLCM,a.QSM,a.CNM]}});hljs.registerLanguage("r",function(a){var b="([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*";return{c:[a.HCM,{b:b,l:b,k:{keyword:"function if in break next repeat else for return switch while try tryCatch|10 stop warning require library attach detach source setMethod setGeneric setGroupGeneric setClass ...|10",literal:"NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10"},r:0},{cN:"number",b:"0[xX][0-9a-fA-F]+[Li]?\\b",r:0},{cN:"number",b:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",r:0},{cN:"number",b:"\\d+\\.(?!\\d)(?:i\\b)?",r:0},{cN:"number",b:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",r:0},{cN:"number",b:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",r:0},{b:"`",e:"`",r:0},{cN:"string",c:[a.BE],v:[{b:'"',e:'"'},{b:"'",e:"'"}]}]}});hljs.registerLanguage("rib",function(a){return{k:"ArchiveRecord AreaLightSource Atmosphere Attribute AttributeBegin AttributeEnd Basis Begin Blobby Bound Clipping ClippingPlane Color ColorSamples ConcatTransform Cone CoordinateSystem CoordSysTransform CropWindow Curves Cylinder DepthOfField Detail DetailRange Disk Displacement Display End ErrorHandler Exposure Exterior Format FrameAspectRatio FrameBegin FrameEnd GeneralPolygon GeometricApproximation Geometry Hider Hyperboloid Identity Illuminate Imager Interior LightSource MakeCubeFaceEnvironment MakeLatLongEnvironment MakeShadow MakeTexture Matte MotionBegin MotionEnd NuPatch ObjectBegin ObjectEnd ObjectInstance Opacity Option Orientation Paraboloid Patch PatchMesh Perspective PixelFilter PixelSamples PixelVariance Points PointsGeneralPolygons PointsPolygons Polygon Procedural Projection Quantize ReadArchive RelativeDetail ReverseOrientation Rotate Scale ScreenWindow ShadingInterpolation ShadingRate Shutter Sides Skew SolidBegin SolidEnd Sphere SubdivisionMesh Surface TextureCoordinates Torus Transform TransformBegin TransformEnd TransformPoints Translate TrimCurve WorldBegin WorldEnd",i:"</",c:[a.HCM,a.CNM,a.ASM,a.QSM]}});hljs.registerLanguage("rsl",function(a){return{k:{keyword:"float color point normal vector matrix while for if do return else break extern continue",built_in:"abs acos ambient area asin atan atmosphere attribute calculatenormal ceil cellnoise clamp comp concat cos degrees depth Deriv diffuse distance Du Dv environment exp faceforward filterstep floor format fresnel incident length lightsource log match max min mod noise normalize ntransform opposite option phong pnoise pow printf ptlined radians random reflect refract renderinfo round setcomp setxcomp setycomp setzcomp shadow sign sin smoothstep specular specularbrdf spline sqrt step tan texture textureinfo trace transform vtransform xcomp ycomp zcomp"},i:"</",c:[a.CLCM,a.CBCM,a.QSM,a.ASM,a.CNM,{cN:"preprocessor",b:"#",e:"$"},{cN:"shader",bK:"surface displacement light volume imager",e:"\\("},{cN:"shading",bK:"illuminate illuminance gather",e:"\\("}]}});hljs.registerLanguage("ruleslanguage",function(a){return{k:{keyword:"BILL_PERIOD BILL_START BILL_STOP RS_EFFECTIVE_START RS_EFFECTIVE_STOP RS_JURIS_CODE RS_OPCO_CODE INTDADDATTRIBUTE|5 INTDADDVMSG|5 INTDBLOCKOP|5 INTDBLOCKOPNA|5 INTDCLOSE|5 INTDCOUNT|5 INTDCOUNTSTATUSCODE|5 INTDCREATEMASK|5 INTDCREATEDAYMASK|5 INTDCREATEFACTORMASK|5 INTDCREATEHANDLE|5 INTDCREATEOVERRIDEDAYMASK|5 INTDCREATEOVERRIDEMASK|5 INTDCREATESTATUSCODEMASK|5 INTDCREATETOUPERIOD|5 INTDDELETE|5 INTDDIPTEST|5 INTDEXPORT|5 INTDGETERRORCODE|5 INTDGETERRORMESSAGE|5 INTDISEQUAL|5 INTDJOIN|5 INTDLOAD|5 INTDLOADACTUALCUT|5 INTDLOADDATES|5 INTDLOADHIST|5 INTDLOADLIST|5 INTDLOADLISTDATES|5 INTDLOADLISTENERGY|5 INTDLOADLISTHIST|5 INTDLOADRELATEDCHANNEL|5 INTDLOADSP|5 INTDLOADSTAGING|5 INTDLOADUOM|5 INTDLOADUOMDATES|5 INTDLOADUOMHIST|5 INTDLOADVERSION|5 INTDOPEN|5 INTDREADFIRST|5 INTDREADNEXT|5 INTDRECCOUNT|5 INTDRELEASE|5 INTDREPLACE|5 INTDROLLAVG|5 INTDROLLPEAK|5 INTDSCALAROP|5 INTDSCALE|5 INTDSETATTRIBUTE|5 INTDSETDSTPARTICIPANT|5 INTDSETSTRING|5 INTDSETVALUE|5 INTDSETVALUESTATUS|5 INTDSHIFTSTARTTIME|5 INTDSMOOTH|5 INTDSORT|5 INTDSPIKETEST|5 INTDSUBSET|5 INTDTOU|5 INTDTOURELEASE|5 INTDTOUVALUE|5 INTDUPDATESTATS|5 INTDVALUE|5 STDEV INTDDELETEEX|5 INTDLOADEXACTUAL|5 INTDLOADEXCUT|5 INTDLOADEXDATES|5 INTDLOADEX|5 INTDLOADEXRELATEDCHANNEL|5 INTDSAVEEX|5 MVLOAD|5 MVLOADACCT|5 MVLOADACCTDATES|5 MVLOADACCTHIST|5 MVLOADDATES|5 MVLOADHIST|5 MVLOADLIST|5 MVLOADLISTDATES|5 MVLOADLISTHIST|5 IF FOR NEXT DONE SELECT END CALL ABORT CLEAR CHANNEL FACTOR LIST NUMBER OVERRIDE SET WEEK DISTRIBUTIONNODE ELSE WHEN THEN OTHERWISE IENUM CSV INCLUDE LEAVE RIDER SAVE DELETE NOVALUE SECTION WARN SAVE_UPDATE DETERMINANT LABEL REPORT REVENUE EACH IN FROM TOTAL CHARGE BLOCK AND OR CSV_FILE RATE_CODE AUXILIARY_DEMAND UIDACCOUNT RS BILL_PERIOD_SELECT HOURS_PER_MONTH INTD_ERROR_STOP SEASON_SCHEDULE_NAME ACCOUNTFACTOR ARRAYUPPERBOUND CALLSTOREDPROC GETADOCONNECTION GETCONNECT GETDATASOURCE GETQUALIFIER GETUSERID HASVALUE LISTCOUNT LISTOP LISTUPDATE LISTVALUE PRORATEFACTOR RSPRORATE SETBINPATH SETDBMONITOR WQ_OPEN BILLINGHOURS DATE DATEFROMFLOAT DATETIMEFROMSTRING DATETIMETOSTRING DATETOFLOAT DAY DAYDIFF DAYNAME DBDATETIME HOUR MINUTE MONTH MONTHDIFF MONTHHOURS MONTHNAME ROUNDDATE SAMEWEEKDAYLASTYEAR SECOND WEEKDAY WEEKDIFF YEAR YEARDAY YEARSTR COMPSUM HISTCOUNT HISTMAX HISTMIN HISTMINNZ HISTVALUE MAXNRANGE MAXRANGE MINRANGE COMPIKVA COMPKVA COMPKVARFROMKQKW COMPLF IDATTR FLAG LF2KW LF2KWH MAXKW POWERFACTOR READING2USAGE AVGSEASON MAXSEASON MONTHLYMERGE SEASONVALUE SUMSEASON ACCTREADDATES ACCTTABLELOAD CONFIGADD CONFIGGET CREATEOBJECT CREATEREPORT EMAILCLIENT EXPBLKMDMUSAGE EXPMDMUSAGE EXPORT_USAGE FACTORINEFFECT GETUSERSPECIFIEDSTOP INEFFECT ISHOLIDAY RUNRATE SAVE_PROFILE SETREPORTTITLE USEREXIT WATFORRUNRATE TO TABLE ACOS ASIN ATAN ATAN2 BITAND CEIL COS COSECANT COSH COTANGENT DIVQUOT DIVREM EXP FABS FLOOR FMOD FREPM FREXPN LOG LOG10 MAX MAXN MIN MINNZ MODF POW ROUND ROUND2VALUE ROUNDINT SECANT SIN SINH SQROOT TAN TANH FLOAT2STRING FLOAT2STRINGNC INSTR LEFT LEN LTRIM MID RIGHT RTRIM STRING STRINGNC TOLOWER TOUPPER TRIM NUMDAYS READ_DATE STAGING",built_in:"IDENTIFIER OPTIONS XML_ELEMENT XML_OP XML_ELEMENT_OF DOMDOCCREATE DOMDOCLOADFILE DOMDOCLOADXML DOMDOCSAVEFILE DOMDOCGETROOT DOMDOCADDPI DOMNODEGETNAME DOMNODEGETTYPE DOMNODEGETVALUE DOMNODEGETCHILDCT DOMNODEGETFIRSTCHILD DOMNODEGETSIBLING DOMNODECREATECHILDELEMENT DOMNODESETATTRIBUTE DOMNODEGETCHILDELEMENTCT DOMNODEGETFIRSTCHILDELEMENT DOMNODEGETSIBLINGELEMENT DOMNODEGETATTRIBUTECT DOMNODEGETATTRIBUTEI DOMNODEGETATTRIBUTEBYNAME DOMNODEGETBYNAME"},c:[a.CLCM,a.CBCM,a.ASM,a.QSM,a.CNM,{cN:"array",b:"#[a-zA-Z .]+"}]}});hljs.registerLanguage("rust",function(a){return{aliases:["rs"],k:{keyword:"alignof as be box break const continue crate do else enum extern false fn for if impl in let loop match mod mut offsetof once priv proc pub pure ref return self sizeof static struct super trait true type typeof unsafe unsized use virtual while yield int i8 i16 i32 i64 uint u8 u32 u64 float f32 f64 str char bool",built_in:"assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! fail! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln!"},l:a.IR+"!?",i:"</",c:[a.CLCM,a.CBCM,a.inherit(a.QSM,{i:null}),{cN:"string",b:/r(#*)".*?"\1(?!#)/},{cN:"string",b:/'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/},{b:/'[a-zA-Z_][a-zA-Z0-9_]*/},{cN:"number",b:"\\b(0[xb][A-Za-z0-9_]+|[0-9_]+(\\.[0-9_]+)?([uif](8|16|32|64)?)?)",r:0},{cN:"function",bK:"fn",e:"(\\(|<)",eE:true,c:[a.UTM]},{cN:"preprocessor",b:"#\\[",e:"\\]"},{bK:"type",e:"(=|<)",c:[a.UTM],i:"\\S"},{bK:"trait enum",e:"({|<)",c:[a.UTM],i:"\\S"},{b:a.IR+"::"},{b:"->"}]}});hljs.registerLanguage("scala",function(d){var b={cN:"annotation",b:"@[A-Za-z]+"};var c={cN:"string",b:'u?r?"""',e:'"""',r:10};var a={cN:"symbol",b:"'\\w[\\w\\d_]*(?!')"};var e={cN:"type",b:"\\b[A-Z][A-Za-z0-9_]*",r:0};var h={cN:"title",b:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,r:0};var i={cN:"class",bK:"class object trait type",e:/[:={\[(\n;]/,c:[{cN:"keyword",bK:"extends with",r:10},h]};var g={cN:"function",bK:"def val",e:/[:={\[(\n;]/,c:[h]};var f={cN:"javadoc",b:"/\\*\\*",e:"\\*/",c:[{cN:"javadoctag",b:"@[A-Za-z]+"}],r:10};return{k:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},c:[d.CLCM,d.CBCM,c,d.QSM,a,e,g,i,d.CNM,b]}});hljs.registerLanguage("scheme",function(k){var m="[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+";var d="(\\-|\\+)?\\d+([./]\\d+)?";var h=d+"[+\\-]"+d+"i";var e={built_in:"case-lambda call/cc class define-class exit-handler field import inherit init-field interface let*-values let-values let/ec mixin opt-lambda override protect provide public rename require require-for-syntax syntax syntax-case syntax-error unit/sig unless when with-syntax and begin call-with-current-continuation call-with-input-file call-with-output-file case cond define define-syntax delay do dynamic-wind else for-each if lambda let let* let-syntax letrec letrec-syntax map or syntax-rules ' * + , ,@ - ... / ; < <= = => > >= ` abs acos angle append apply asin assoc assq assv atan boolean? caar cadr call-with-input-file call-with-output-file call-with-values car cdddar cddddr cdr ceiling char->integer char-alphabetic? char-ci<=? char-ci<? char-ci=? char-ci>=? char-ci>? char-downcase char-lower-case? char-numeric? char-ready? char-upcase char-upper-case? char-whitespace? char<=? char<? char=? char>=? char>? char? close-input-port close-output-port complex? cons cos current-input-port current-output-port denominator display eof-object? eq? equal? eqv? eval even? exact->inexact exact? exp expt floor force gcd imag-part inexact->exact inexact? input-port? integer->char integer? interaction-environment lcm length list list->string list->vector list-ref list-tail list? load log magnitude make-polar make-rectangular make-string make-vector max member memq memv min modulo negative? newline not null-environment null? number->string number? numerator odd? open-input-file open-output-file output-port? pair? peek-char port? positive? procedure? quasiquote quote quotient rational? rationalize read read-char real-part real? remainder reverse round scheme-report-environment set! set-car! set-cdr! sin sqrt string string->list string->number string->symbol string-append string-ci<=? string-ci<? string-ci=? string-ci>=? string-ci>? string-copy string-fill! string-length string-ref string-set! string<=? string<? string=? string>=? string>? string? substring symbol->string symbol? tan transcript-off transcript-on truncate values vector vector->list vector-fill! vector-length vector-ref vector-set! with-input-from-file with-output-to-file write write-char zero?"};var n={cN:"shebang",b:"^#!",e:"$"};var f={cN:"literal",b:"(#t|#f|#\\\\"+m+"|#\\\\.)"};var g={cN:"number",v:[{b:d,r:0},{b:h,r:0},{b:"#b[0-1]+(/[0-1]+)?"},{b:"#o[0-7]+(/[0-7]+)?"},{b:"#x[0-9a-f]+(/[0-9a-f]+)?"}]};var j=k.QSM;var b={cN:"regexp",b:'#[pr]x"',e:'[^\\\\]"'};var o={cN:"comment",v:[{b:";",e:"$",r:0},{b:"#\\|",e:"\\|#"}]};var c={b:m,r:0};var a={cN:"variable",b:"'"+m};var i={eW:true,r:0};var l={cN:"list",v:[{b:"\\(",e:"\\)"},{b:"\\[",e:"\\]"}],c:[{cN:"keyword",b:m,l:m,k:e},i]};i.c=[f,g,j,o,c,a,l];return{i:/\S/,c:[n,g,j,o,a,l]}});hljs.registerLanguage("scilab",function(a){var b=[a.CNM,{cN:"string",b:"'|\"",e:"'|\"",c:[a.BE,{b:"''"}]}];return{aliases:["sci"],k:{keyword:"abort break case clear catch continue do elseif else endfunction end for functionglobal if pause return resume select try then while%f %F %t %T %pi %eps %inf %nan %e %i %z %s",built_in:"abs and acos asin atan ceil cd chdir clearglobal cosh cos cumprod deff disp errorexec execstr exists exp eye gettext floor fprintf fread fsolve imag isdef isemptyisinfisnan isvector lasterror length load linspace list listfiles log10 log2 logmax min msprintf mclose mopen ones or pathconvert poly printf prod pwd rand realround sinh sin size gsort sprintf sqrt strcat strcmps tring sum system tanh tantype typename warning zeros matrix"},i:'("|#|/\\*|\\s+/\\w+)',c:[{cN:"function",bK:"function endfunction",e:"$",k:"function endfunction|10",c:[a.UTM,{cN:"params",b:"\\(",e:"\\)"}]},{cN:"transposed_variable",b:"[a-zA-Z_][a-zA-Z_0-9]*('+[\\.']*|[\\.']+)",e:"",r:0},{cN:"matrix",b:"\\[",e:"\\]'*[\\.']*",r:0,c:b},{cN:"comment",b:"//",e:"$"}].concat(b)}});hljs.registerLanguage("scss",function(a){var c="[a-zA-Z-][a-zA-Z0-9_-]*";var f={cN:"variable",b:"(\\$"+c+")\\b"};var d={cN:"function",b:c+"\\(",rB:true,eE:true,e:"\\("};var b={cN:"hexcolor",b:"#[0-9A-Fa-f]+"};var e={cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:true,i:"[^\\s]",starts:{cN:"value",eW:true,eE:true,c:[d,b,a.CSSNM,a.QSM,a.ASM,a.CBCM,{cN:"important",b:"!important"}]}};return{cI:true,i:"[=/|']",c:[a.CLCM,a.CBCM,d,{cN:"id",b:"\\#[A-Za-z0-9_-]+",r:0},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"tag",b:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",r:0},{cN:"pseudo",b:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{cN:"pseudo",b:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},f,{cN:"attribute",b:"\\b(z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",i:"[^\\s]"},{cN:"value",b:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{cN:"value",b:":",e:";",c:[d,f,b,a.CSSNM,a.QSM,a.ASM,{cN:"important",b:"!important"}]},{cN:"at_rule",b:"@",e:"[{;]",k:"mixin include extend for if else each while charset import debug media page content font-face namespace warn",c:[d,f,a.QSM,a.ASM,b,a.CSSNM,{cN:"preprocessor",b:"\\s[A-Za-z0-9_.-]+",r:0}]}]}});hljs.registerLanguage("smalltalk",function(a){var b="[a-z][a-zA-Z0-9_]*";var d={cN:"char",b:"\\$.{1}"};var c={cN:"symbol",b:"#"+a.UIR};return{aliases:["st"],k:"self super nil true false thisContext",c:[{cN:"comment",b:'"',e:'"'},a.ASM,{cN:"class",b:"\\b[A-Z][A-Za-z0-9_]*",r:0},{cN:"method",b:b+":",r:0},a.CNM,c,d,{cN:"localvars",b:"\\|[ ]*"+b+"([ ]+"+b+")*[ ]*\\|",rB:true,e:/\|/,i:/\S/,c:[{b:"(\\|[ ]*)?"+b}]},{cN:"array",b:"\\#\\(",e:"\\)",c:[a.ASM,d,a.CNM,c]}]}});hljs.registerLanguage("sql",function(a){var b={cN:"comment",b:"--",e:"$"};return{cI:true,i:/[<>]/,c:[{cN:"operator",bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate savepoint release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup",e:/;/,eW:true,k:{keyword:"abs absolute acos action add adddate addtime aes_decrypt aes_encrypt after aggregate all allocate alter analyze and any are as asc ascii asin assertion at atan atan2 atn2 authorization authors avg backup before begin benchmark between bin binlog bit_and bit_count bit_length bit_or bit_xor both by cache call cascade cascaded case cast catalog ceil ceiling chain change changed char_length character_length charindex charset check checksum checksum_agg choose close coalesce coercibility collate collation collationproperty column columns columns_updated commit compress concat concat_ws concurrent connect connection connection_id consistent constraint constraints continue contributors conv convert convert_tz corresponding cos cot count count_big crc32 create cross cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime data database databases datalength date_add date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts datetimeoffsetfromparts day dayname dayofmonth dayofweek dayofyear deallocate declare decode default deferrable deferred degrees delayed delete des_decrypt des_encrypt des_key_file desc describe descriptor diagnostics difference disconnect distinct distinctrow div do domain double drop dumpfile each else elt enclosed encode encrypt end end-exec engine engines eomonth errors escape escaped event eventdata events except exception exec execute exists exp explain export_set extended external extract fast fetch field fields find_in_set first first_value floor flush for force foreign format found found_rows from from_base64 from_days from_unixtime full function get get_format get_lock getdate getutcdate global go goto grant grants greatest group group_concat grouping grouping_id gtid_subset gtid_subtract handler having help hex high_priority hosts hour ident_current ident_incr ident_seed identified identity if ifnull ignore iif ilike immediate in index indicator inet6_aton inet6_ntoa inet_aton inet_ntoa infile initially inner innodb input insert install instr intersect into is is_free_lock is_ipv4 is_ipv4_compat is_ipv4_mapped is_not is_not_null is_used_lock isdate isnull isolation join key kill language last last_day last_insert_id last_value lcase lead leading least leaves left len lenght level like limit lines ln load load_file local localtime localtimestamp locate lock log log10 log2 logfile logs low_priority lower lpad ltrim make_set makedate maketime master master_pos_wait match matched max md5 medium merge microsecond mid min minute mod mode module month monthname mutex name_const names national natural nchar next no no_write_to_binlog not now nullif nvarchar oct octet_length of old_password on only open optimize option optionally or ord order outer outfile output pad parse partial partition password patindex percent_rank percentile_cont percentile_disc period_add period_diff pi plugin position pow power pragma precision prepare preserve primary prior privileges procedure procedure_analyze processlist profile profiles public publishingservername purge quarter query quick quote quotename radians rand read references regexp relative relaylog release release_lock rename repair repeat replace replicate reset restore restrict return returns reverse revoke right rlike rollback rollup round row row_count rows rpad rtrim savepoint schema scroll sec_to_time second section select serializable server session session_user set sha sha1 sha2 share show sign sin size slave sleep smalldatetimefromparts snapshot some soname soundex sounds_like space sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_no_cache sql_small_result sql_variant_property sqlstate sqrt square start starting status std stddev stddev_pop stddev_samp stdev stdevp stop str str_to_date straight_join strcmp string stuff subdate substr substring subtime subtring_index sum switchoffset sysdate sysdatetime sysdatetimeoffset system_user sysutcdatetime table tables tablespace tan temporary terminated tertiary_weights then time time_format time_to_sec timediff timefromparts timestamp timestampadd timestampdiff timezone_hour timezone_minute to to_base64 to_days to_seconds todatetimeoffset trailing transaction translation trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse ucase uncompress uncompressed_length unhex unicode uninstall union unique unix_timestamp unknown unlock update upgrade upped upper usage use user user_resources using utc_date utc_time utc_timestamp uuid uuid_short validate_password_strength value values var var_pop var_samp variables variance varp version view warnings week weekday weekofyear weight_string when whenever where with work write xml xor year yearweek zon",literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int integer interval number numeric real serial smallint varchar varying int8 serial8 text"},c:[{cN:"string",b:"'",e:"'",c:[a.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[a.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[a.BE]},a.CNM,a.CBCM,b]},a.CBCM,b]}});hljs.registerLanguage("swift",function(a){var e={keyword:"class deinit enum extension func import init let protocol static struct subscript typealias var break case continue default do else fallthrough if in for return switch where while as dynamicType is new super self Self Type __COLUMN__ __FILE__ __FUNCTION__ __LINE__ associativity didSet get infix inout left mutating none nonmutating operator override postfix precedence prefix right set unowned unowned safe unsafe weak willSet",literal:"true false nil",built_in:"abs advance alignof alignofValue assert bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal false filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced join lexicographicalCompare map max maxElement min minElement nil numericCast partition posix print println quickSort reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith strideof strideofValue swap swift toString transcode true underestimateCount unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafePointers withVaList"};var g={cN:"type",b:"\\b[A-Z][\\w']*",r:0};var b={cN:"comment",b:"/\\*",e:"\\*/",c:[a.PWM,"self"]};var c={cN:"subst",b:/\\\(/,e:"\\)",k:e,c:[]};var f={cN:"number",b:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",r:0};var d=a.inherit(a.QSM,{c:[c,a.BE]});c.c=[f];return{k:e,c:[d,a.CLCM,b,g,f,{cN:"func",bK:"func",e:"{",eE:true,c:[a.inherit(a.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/,i:/\(/}),{cN:"generics",b:/\</,e:/\>/,i:/\>/},{cN:"params",b:/\(/,e:/\)/,k:e,c:["self",f,d,a.CBCM,{b:":"}],i:/["']/}],i:/\[|%/},{cN:"class",k:"struct protocol class extension enum",b:"(struct|protocol|class(?! (func|var))|extension|enum)",e:"\\{",eE:true,c:[a.inherit(a.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/})]},{cN:"preprocessor",b:"(@assignment|@class_protocol|@exported|@final|@lazy|@noreturn|@NSCopying|@NSManaged|@objc|@optional|@required|@auto_closure|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix)"},]}});hljs.registerLanguage("tex",function(a){var d={cN:"command",b:"\\\\[a-zA-Zа-яА-я]+[\\*]?"};var c={cN:"command",b:"\\\\[^a-zA-Zа-яА-я0-9]"};var b={cN:"special",b:"[{}\\[\\]\\&#~]",r:0};return{c:[{b:"\\\\[a-zA-Zа-яА-я]+[\\*]? *= *-?\\d*\\.?\\d+(pt|pc|mm|cm|in|dd|cc|ex|em)?",rB:true,c:[d,c,{cN:"number",b:" *=",e:"-?\\d*\\.?\\d+(pt|pc|mm|cm|in|dd|cc|ex|em)?",eB:true}],r:10},d,c,b,{cN:"formula",b:"\\$\\$",e:"\\$\\$",c:[d,c,b],r:0},{cN:"formula",b:"\\$",e:"\\$",c:[d,c,b],r:0},{cN:"comment",b:"%",e:"$",r:0}]}});hljs.registerLanguage("thrift",function(a){var b="bool byte i16 i32 i64 double string binary";return{k:{keyword:"namespace const typedef struct enum service exception void oneway set list map required optional",built_in:b,literal:"true false"},c:[a.QSM,a.NM,a.CLCM,a.CBCM,{cN:"class",bK:"struct enum service exception",e:/\{/,i:/\n/,c:[a.inherit(a.TM,{starts:{eW:true,eE:true}})]},{cN:"stl_container",b:"\\b(set|list|map)\\s*<",e:">",k:b,c:["self"]}]}});hljs.registerLanguage("typescript",function(a){return{aliases:["ts"],k:{keyword:"in if for while finally var new function|0 do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private get set super interface extendsstatic constructor implements enum export import declare",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void",},c:[{cN:"pi",b:/^\s*('|")use strict('|")/,r:0},a.ASM,a.QSM,a.CLCM,a.CBCM,a.CNM,{b:"("+a.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[a.CLCM,a.CBCM,a.RM,{b:/</,e:/>;/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:true,c:[a.inherit(a.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,c:[a.CLCM,a.CBCM],i:/["'\(]/}],i:/\[|%/,r:0},{cN:"constructor",bK:"constructor",e:/\{/,eE:true,r:10},{cN:"module",bK:"module",e:/\{/,eE:true,},{cN:"interface",bK:"interface",e:/\{/,eE:true,},{b:/\$[(.]/},{b:"\\."+a.IR,r:0}]}});hljs.registerLanguage("vala",function(a){return{k:{keyword:"char uchar unichar int uint long ulong short ushort int8 int16 int32 int64 uint8 uint16 uint32 uint64 float double bool struct enum string void weak unowned owned async signal static abstract interface override while do for foreach else switch case break default return try catch public private protected internal using new this get set const stdout stdin stderr var",built_in:"DBus GLib CCode Gee Object",literal:"false true null"},c:[{cN:"class",bK:"class interface delegate namespace",e:"{",eE:true,i:"[^,:\\n\\s\\.]",c:[a.UTM]},a.CLCM,a.CBCM,{cN:"string",b:'"""',e:'"""',r:5},a.ASM,a.QSM,a.CNM,{cN:"preprocessor",b:"^#",e:"$",r:2},{cN:"constant",b:" [A-Z_]+ ",r:0}]}});hljs.registerLanguage("vbnet",function(a){return{aliases:["vb"],cI:true,k:{keyword:"addhandler addressof alias and andalso aggregate ansi as assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into is isfalse isnot istrue join key let lib like loop me mid mod module mustinherit mustoverride mybase myclass namespace narrowing new next not notinheritable notoverridable of off on operator option optional or order orelse overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim rem removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly xor",built_in:"boolean byte cbool cbyte cchar cdate cdec cdbl char cint clng cobj csbyte cshort csng cstr ctype date decimal directcast double gettype getxmlnamespace iif integer long object sbyte short single string trycast typeof uinteger ulong ushort",literal:"true false nothing"},i:"//|{|}|endif|gosub|variant|wend",c:[a.inherit(a.QSM,{c:[{b:'""'}]}),{cN:"comment",b:"'",e:"$",rB:true,c:[{cN:"xmlDocTag",b:"'''|<!--|-->"},{cN:"xmlDocTag",b:"</?",e:">"}]},a.CNM,{cN:"preprocessor",b:"#",e:"$",k:"if else elseif end region externalsource"}]}});hljs.registerLanguage("vbscript",function(a){return{aliases:["vbs"],cI:true,k:{keyword:"call class const dim do loop erase execute executeglobal exit for each next function if then else on error option explicit new private property let get public randomize redim rem select case set stop sub while wend with end to elseif is or xor and not class_initialize class_terminate default preserve in me byval byref step resume goto",built_in:"lcase month vartype instrrev ubound setlocale getobject rgb getref string weekdayname rnd dateadd monthname now day minute isarray cbool round formatcurrency conversions csng timevalue second year space abs clng timeserial fixs len asc isempty maths dateserial atn timer isobject filter weekday datevalue ccur isdate instr datediff formatdatetime replace isnull right sgn array snumeric log cdbl hex chr lbound msgbox ucase getlocale cos cdate cbyte rtrim join hour oct typename trim strcomp int createobject loadpicture tan formatnumber mid scriptenginebuildversion scriptengine split scriptengineminorversion cint sin datepart ltrim sqr scriptenginemajorversion time derived eval date formatpercent exp inputbox left ascw chrw regexp server response request cstr err",literal:"true false null nothing empty"},i:"//",c:[a.inherit(a.QSM,{c:[{b:'""'}]}),{cN:"comment",b:/'/,e:/$/,r:0},a.CNM]}});hljs.registerLanguage("vhdl",function(a){return{cI:true,k:{keyword:"abs access after alias all and architecture array assert attribute begin block body buffer bus case component configuration constant context cover disconnect downto default else elsif end entity exit fairness file for force function generate generic group guarded if impure in inertial inout is label library linkage literal loop map mod nand new next nor not null of on open or others out package port postponed procedure process property protected pure range record register reject release rem report restrict restrict_guarantee return rol ror select sequence severity shared signal sla sll sra srl strong subtype then to transport type unaffected units until use variable vmode vprop vunit wait when while with xnor xor",typename:"boolean bit character severity_level integer time delay_length natural positive string bit_vector file_open_kind file_open_status std_ulogic std_ulogic_vector std_logic std_logic_vector unsigned signed boolean_vector integer_vector real_vector time_vector"},i:"{",c:[a.CBCM,{cN:"comment",b:"--",e:"$"},a.QSM,a.CNM,{cN:"literal",b:"'(U|X|0|1|Z|W|L|H|-)'",c:[a.BE]},{cN:"attribute",b:"'[A-Za-z](_?[A-Za-z0-9])*",c:[a.BE]}]}});hljs.registerLanguage("vim",function(a){return{l:/[!#@\w]+/,k:{keyword:"N|0 P|0 X|0 a|0 ab abc abo al am an|0 ar arga argd arge argdo argg argl argu as au aug aun b|0 bN ba bad bd be bel bf bl bm bn bo bp br brea breaka breakd breakl bro bufdo buffers bun bw c|0 cN cNf ca cabc caddb cad caddf cal cat cb cc ccl cd ce cex cf cfir cgetb cgete cg changes chd che checkt cl cla clo cm cmapc cme cn cnew cnf cno cnorea cnoreme co col colo com comc comp con conf cope cp cpf cq cr cs cst cu cuna cunme cw d|0 delm deb debugg delc delf dif diffg diffo diffp diffpu diffs diffthis dig di dl dell dj dli do doautoa dp dr ds dsp e|0 ea ec echoe echoh echom echon el elsei em en endfo endf endt endw ene ex exe exi exu f|0 files filet fin fina fini fir fix fo foldc foldd folddoc foldo for fu g|0 go gr grepa gu gv ha h|0 helpf helpg helpt hi hid his i|0 ia iabc if ij il im imapc ime ino inorea inoreme int is isp iu iuna iunme j|0 ju k|0 keepa kee keepj lN lNf l|0 lad laddb laddf la lan lat lb lc lch lcl lcs le lefta let lex lf lfir lgetb lgete lg lgr lgrepa lh ll lla lli lmak lm lmapc lne lnew lnf ln loadk lo loc lockv lol lope lp lpf lr ls lt lu lua luad luaf lv lvimgrepa lw m|0 ma mak map mapc marks mat me menut mes mk mks mksp mkv mkvie mod mz mzf nbc nb nbs n|0 new nm nmapc nme nn nnoreme noa no noh norea noreme norm nu nun nunme ol o|0 om omapc ome on ono onoreme opt ou ounme ow p|0 profd prof pro promptr pc ped pe perld po popu pp pre prev ps pt ptN ptf ptj ptl ptn ptp ptr pts pu pw py3 python3 py3d py3f py pyd pyf q|0 quita qa r|0 rec red redi redr redraws reg res ret retu rew ri rightb rub rubyd rubyf rund ru rv s|0 sN san sa sal sav sb sbN sba sbf sbl sbm sbn sbp sbr scrip scripte scs se setf setg setl sf sfir sh sim sig sil sl sla sm smap smapc sme sn sni sno snor snoreme sor so spelld spe spelli spellr spellu spellw sp spr sre st sta startg startr star stopi stj sts sun sunm sunme sus sv sw sy synti sync t|0 tN tabN tabc tabdo tabe tabf tabfir tabl tabm tabnew tabn tabo tabp tabr tabs tab ta tags tc tcld tclf te tf th tj tl tm tn to tp tr try ts tu u|0 undoj undol una unh unl unlo unm unme uns up v|0 ve verb vert vim vimgrepa vi viu vie vm vmapc vme vne vn vnoreme vs vu vunme windo w|0 wN wa wh wi winc winp wn wp wq wqa ws wu wv x|0 xa xmapc xm xme xn xnoreme xu xunme y|0 z|0 ~ Next Print append abbreviate abclear aboveleft all amenu anoremenu args argadd argdelete argedit argglobal arglocal argument ascii autocmd augroup aunmenu buffer bNext ball badd bdelete behave belowright bfirst blast bmodified bnext botright bprevious brewind break breakadd breakdel breaklist browse bunload bwipeout change cNext cNfile cabbrev cabclear caddbuffer caddexpr caddfile call catch cbuffer cclose center cexpr cfile cfirst cgetbuffer cgetexpr cgetfile chdir checkpath checktime clist clast close cmap cmapclear cmenu cnext cnewer cnfile cnoremap cnoreabbrev cnoremenu copy colder colorscheme command comclear compiler continue confirm copen cprevious cpfile cquit crewind cscope cstag cunmap cunabbrev cunmenu cwindow delete delmarks debug debuggreedy delcommand delfunction diffupdate diffget diffoff diffpatch diffput diffsplit digraphs display deletel djump dlist doautocmd doautoall deletep drop dsearch dsplit edit earlier echo echoerr echohl echomsg else elseif emenu endif endfor endfunction endtry endwhile enew execute exit exusage file filetype find finally finish first fixdel fold foldclose folddoopen folddoclosed foldopen function global goto grep grepadd gui gvim hardcopy help helpfind helpgrep helptags highlight hide history insert iabbrev iabclear ijump ilist imap imapclear imenu inoremap inoreabbrev inoremenu intro isearch isplit iunmap iunabbrev iunmenu join jumps keepalt keepmarks keepjumps lNext lNfile list laddexpr laddbuffer laddfile last language later lbuffer lcd lchdir lclose lcscope left leftabove lexpr lfile lfirst lgetbuffer lgetexpr lgetfile lgrep lgrepadd lhelpgrep llast llist lmake lmap lmapclear lnext lnewer lnfile lnoremap loadkeymap loadview lockmarks lockvar lolder lopen lprevious lpfile lrewind ltag lunmap luado luafile lvimgrep lvimgrepadd lwindow move mark make mapclear match menu menutranslate messages mkexrc mksession mkspell mkvimrc mkview mode mzscheme mzfile nbclose nbkey nbsart next nmap nmapclear nmenu nnoremap nnoremenu noautocmd noremap nohlsearch noreabbrev noremenu normal number nunmap nunmenu oldfiles open omap omapclear omenu only onoremap onoremenu options ounmap ounmenu ownsyntax print profdel profile promptfind promptrepl pclose pedit perl perldo pop popup ppop preserve previous psearch ptag ptNext ptfirst ptjump ptlast ptnext ptprevious ptrewind ptselect put pwd py3do py3file python pydo pyfile quit quitall qall read recover redo redir redraw redrawstatus registers resize retab return rewind right rightbelow ruby rubydo rubyfile rundo runtime rviminfo substitute sNext sandbox sargument sall saveas sbuffer sbNext sball sbfirst sblast sbmodified sbnext sbprevious sbrewind scriptnames scriptencoding scscope set setfiletype setglobal setlocal sfind sfirst shell simalt sign silent sleep slast smagic smapclear smenu snext sniff snomagic snoremap snoremenu sort source spelldump spellgood spellinfo spellrepall spellundo spellwrong split sprevious srewind stop stag startgreplace startreplace startinsert stopinsert stjump stselect sunhide sunmap sunmenu suspend sview swapname syntax syntime syncbind tNext tabNext tabclose tabedit tabfind tabfirst tablast tabmove tabnext tabonly tabprevious tabrewind tag tcl tcldo tclfile tearoff tfirst throw tjump tlast tmenu tnext topleft tprevious trewind tselect tunmenu undo undojoin undolist unabbreviate unhide unlet unlockvar unmap unmenu unsilent update vglobal version verbose vertical vimgrep vimgrepadd visual viusage view vmap vmapclear vmenu vnew vnoremap vnoremenu vsplit vunmap vunmenu write wNext wall while winsize wincmd winpos wnext wprevious wqall wsverb wundo wviminfo xit xall xmapclear xmap xmenu xnoremap xnoremenu xunmap xunmenu yank",built_in:"abs acos add and append argc argidx argv asin atan atan2 browse browsedir bufexists buflisted bufloaded bufname bufnr bufwinnr byte2line byteidx call ceil changenr char2nr cindent clearmatches col complete complete_add complete_check confirm copy cos cosh count cscope_connection cursor deepcopy delete did_filetype diff_filler diff_hlID empty escape eval eventhandler executable exists exp expand extend feedkeys filereadable filewritable filter finddir findfile float2nr floor fmod fnameescape fnamemodify foldclosed foldclosedend foldlevel foldtext foldtextresult foreground function garbagecollect get getbufline getbufvar getchar getcharmod getcmdline getcmdpos getcmdtype getcwd getfontname getfperm getfsize getftime getftype getline getloclist getmatches getpid getpos getqflist getreg getregtype gettabvar gettabwinvar getwinposx getwinposy getwinvar glob globpath has has_key haslocaldir hasmapto histadd histdel histget histnr hlexists hlID hostname iconv indent index input inputdialog inputlist inputrestore inputsave inputsecret insert invert isdirectory islocked items join keys len libcall libcallnr line line2byte lispindent localtime log log10 luaeval map maparg mapcheck match matchadd matcharg matchdelete matchend matchlist matchstr max min mkdir mode mzeval nextnonblank nr2char or pathshorten pow prevnonblank printf pumvisible py3eval pyeval range readfile reltime reltimestr remote_expr remote_foreground remote_peek remote_read remote_send remove rename repeat resolve reverse round screenattr screenchar screencol screenrow search searchdecl searchpair searchpairpos searchpos server2client serverlist setbufvar setcmdpos setline setloclist setmatches setpos setqflist setreg settabvar settabwinvar setwinvar sha256 shellescape shiftwidth simplify sin sinh sort soundfold spellbadword spellsuggest split sqrt str2float str2nr strchars strdisplaywidth strftime stridx string strlen strpart strridx strtrans strwidth submatch substitute synconcealed synID synIDattr synIDtrans synstack system tabpagebuflist tabpagenr tabpagewinnr tagfiles taglist tan tanh tempname tolower toupper tr trunc type undofile undotree values virtcol visualmode wildmenumode winbufnr wincol winheight winline winnr winrestcmd winrestview winsaveview winwidth writefile xor"},i:/[{:]/,c:[a.NM,a.ASM,{cN:"string",b:/"((\\")|[^"\n])*("|\n)/},{cN:"variable",b:/[bwtglsav]:[\w\d_]*/},{cN:"function",bK:"function function!",e:"$",r:0,c:[a.TM,{cN:"params",b:"\\(",e:"\\)"}]}]}});hljs.registerLanguage("x86asm",function(a){return{cI:true,l:"\\.?"+a.IR,k:{keyword:"lock rep repe repz repne repnz xaquire xrelease bnd nobnd aaa aad aam aas adc add and arpl bb0_reset bb1_reset bound bsf bsr bswap bt btc btr bts call cbw cdq cdqe clc cld cli clts cmc cmp cmpsb cmpsd cmpsq cmpsw cmpxchg cmpxchg486 cmpxchg8b cmpxchg16b cpuid cpu_read cpu_write cqo cwd cwde daa das dec div dmint emms enter equ f2xm1 fabs fadd faddp fbld fbstp fchs fclex fcmovb fcmovbe fcmove fcmovnb fcmovnbe fcmovne fcmovnu fcmovu fcom fcomi fcomip fcomp fcompp fcos fdecstp fdisi fdiv fdivp fdivr fdivrp femms feni ffree ffreep fiadd ficom ficomp fidiv fidivr fild fimul fincstp finit fist fistp fisttp fisub fisubr fld fld1 fldcw fldenv fldl2e fldl2t fldlg2 fldln2 fldpi fldz fmul fmulp fnclex fndisi fneni fninit fnop fnsave fnstcw fnstenv fnstsw fpatan fprem fprem1 fptan frndint frstor fsave fscale fsetpm fsin fsincos fsqrt fst fstcw fstenv fstp fstsw fsub fsubp fsubr fsubrp ftst fucom fucomi fucomip fucomp fucompp fxam fxch fxtract fyl2x fyl2xp1 hlt ibts icebp idiv imul in inc incbin insb insd insw int int01 int1 int03 int3 into invd invpcid invlpg invlpga iret iretd iretq iretw jcxz jecxz jrcxz jmp jmpe lahf lar lds lea leave les lfence lfs lgdt lgs lidt lldt lmsw loadall loadall286 lodsb lodsd lodsq lodsw loop loope loopne loopnz loopz lsl lss ltr mfence monitor mov movd movq movsb movsd movsq movsw movsx movsxd movzx mul mwait neg nop not or out outsb outsd outsw packssdw packsswb packuswb paddb paddd paddsb paddsiw paddsw paddusb paddusw paddw pand pandn pause paveb pavgusb pcmpeqb pcmpeqd pcmpeqw pcmpgtb pcmpgtd pcmpgtw pdistib pf2id pfacc pfadd pfcmpeq pfcmpge pfcmpgt pfmax pfmin pfmul pfrcp pfrcpit1 pfrcpit2 pfrsqit1 pfrsqrt pfsub pfsubr pi2fd pmachriw pmaddwd pmagw pmulhriw pmulhrwa pmulhrwc pmulhw pmullw pmvgezb pmvlzb pmvnzb pmvzb pop popa popad popaw popf popfd popfq popfw por prefetch prefetchw pslld psllq psllw psrad psraw psrld psrlq psrlw psubb psubd psubsb psubsiw psubsw psubusb psubusw psubw punpckhbw punpckhdq punpckhwd punpcklbw punpckldq punpcklwd push pusha pushad pushaw pushf pushfd pushfq pushfw pxor rcl rcr rdshr rdmsr rdpmc rdtsc rdtscp ret retf retn rol ror rdm rsdc rsldt rsm rsts sahf sal salc sar sbb scasb scasd scasq scasw sfence sgdt shl shld shr shrd sidt sldt skinit smi smint smintold smsw stc std sti stosb stosd stosq stosw str sub svdc svldt svts swapgs syscall sysenter sysexit sysret test ud0 ud1 ud2b ud2 ud2a umov verr verw fwait wbinvd wrshr wrmsr xadd xbts xchg xlatb xlat xor cmove cmovz cmovne cmovnz cmova cmovnbe cmovae cmovnb cmovb cmovnae cmovbe cmovna cmovg cmovnle cmovge cmovnl cmovl cmovnge cmovle cmovng cmovc cmovnc cmovo cmovno cmovs cmovns cmovp cmovpe cmovnp cmovpo je jz jne jnz ja jnbe jae jnb jb jnae jbe jna jg jnle jge jnl jl jnge jle jng jc jnc jo jno js jns jpo jnp jpe jp sete setz setne setnz seta setnbe setae setnb setnc setb setnae setcset setbe setna setg setnle setge setnl setl setnge setle setng sets setns seto setno setpe setp setpo setnp addps addss andnps andps cmpeqps cmpeqss cmpleps cmpless cmpltps cmpltss cmpneqps cmpneqss cmpnleps cmpnless cmpnltps cmpnltss cmpordps cmpordss cmpunordps cmpunordss cmpps cmpss comiss cvtpi2ps cvtps2pi cvtsi2ss cvtss2si cvttps2pi cvttss2si divps divss ldmxcsr maxps maxss minps minss movaps movhps movlhps movlps movhlps movmskps movntps movss movups mulps mulss orps rcpps rcpss rsqrtps rsqrtss shufps sqrtps sqrtss stmxcsr subps subss ucomiss unpckhps unpcklps xorps fxrstor fxrstor64 fxsave fxsave64 xgetbv xsetbv xsave xsave64 xsaveopt xsaveopt64 xrstor xrstor64 prefetchnta prefetcht0 prefetcht1 prefetcht2 maskmovq movntq pavgb pavgw pextrw pinsrw pmaxsw pmaxub pminsw pminub pmovmskb pmulhuw psadbw pshufw pf2iw pfnacc pfpnacc pi2fw pswapd maskmovdqu clflush movntdq movnti movntpd movdqa movdqu movdq2q movq2dq paddq pmuludq pshufd pshufhw pshuflw pslldq psrldq psubq punpckhqdq punpcklqdq addpd addsd andnpd andpd cmpeqpd cmpeqsd cmplepd cmplesd cmpltpd cmpltsd cmpneqpd cmpneqsd cmpnlepd cmpnlesd cmpnltpd cmpnltsd cmpordpd cmpordsd cmpunordpd cmpunordsd cmppd comisd cvtdq2pd cvtdq2ps cvtpd2dq cvtpd2pi cvtpd2ps cvtpi2pd cvtps2dq cvtps2pd cvtsd2si cvtsd2ss cvtsi2sd cvtss2sd cvttpd2pi cvttpd2dq cvttps2dq cvttsd2si divpd divsd maxpd maxsd minpd minsd movapd movhpd movlpd movmskpd movupd mulpd mulsd orpd shufpd sqrtpd sqrtsd subpd subsd ucomisd unpckhpd unpcklpd xorpd addsubpd addsubps haddpd haddps hsubpd hsubps lddqu movddup movshdup movsldup clgi stgi vmcall vmclear vmfunc vmlaunch vmload vmmcall vmptrld vmptrst vmread vmresume vmrun vmsave vmwrite vmxoff vmxon invept invvpid pabsb pabsw pabsd palignr phaddw phaddd phaddsw phsubw phsubd phsubsw pmaddubsw pmulhrsw pshufb psignb psignw psignd extrq insertq movntsd movntss lzcnt blendpd blendps blendvpd blendvps dppd dpps extractps insertps movntdqa mpsadbw packusdw pblendvb pblendw pcmpeqq pextrb pextrd pextrq phminposuw pinsrb pinsrd pinsrq pmaxsb pmaxsd pmaxud pmaxuw pminsb pminsd pminud pminuw pmovsxbw pmovsxbd pmovsxbq pmovsxwd pmovsxwq pmovsxdq pmovzxbw pmovzxbd pmovzxbq pmovzxwd pmovzxwq pmovzxdq pmuldq pmulld ptest roundpd roundps roundsd roundss crc32 pcmpestri pcmpestrm pcmpistri pcmpistrm pcmpgtq popcnt getsec pfrcpv pfrsqrtv movbe aesenc aesenclast aesdec aesdeclast aesimc aeskeygenassist vaesenc vaesenclast vaesdec vaesdeclast vaesimc vaeskeygenassist vaddpd vaddps vaddsd vaddss vaddsubpd vaddsubps vandpd vandps vandnpd vandnps vblendpd vblendps vblendvpd vblendvps vbroadcastss vbroadcastsd vbroadcastf128 vcmpeq_ospd vcmpeqpd vcmplt_ospd vcmpltpd vcmple_ospd vcmplepd vcmpunord_qpd vcmpunordpd vcmpneq_uqpd vcmpneqpd vcmpnlt_uspd vcmpnltpd vcmpnle_uspd vcmpnlepd vcmpord_qpd vcmpordpd vcmpeq_uqpd vcmpnge_uspd vcmpngepd vcmpngt_uspd vcmpngtpd vcmpfalse_oqpd vcmpfalsepd vcmpneq_oqpd vcmpge_ospd vcmpgepd vcmpgt_ospd vcmpgtpd vcmptrue_uqpd vcmptruepd vcmplt_oqpd vcmple_oqpd vcmpunord_spd vcmpneq_uspd vcmpnlt_uqpd vcmpnle_uqpd vcmpord_spd vcmpeq_uspd vcmpnge_uqpd vcmpngt_uqpd vcmpfalse_ospd vcmpneq_ospd vcmpge_oqpd vcmpgt_oqpd vcmptrue_uspd vcmppd vcmpeq_osps vcmpeqps vcmplt_osps vcmpltps vcmple_osps vcmpleps vcmpunord_qps vcmpunordps vcmpneq_uqps vcmpneqps vcmpnlt_usps vcmpnltps vcmpnle_usps vcmpnleps vcmpord_qps vcmpordps vcmpeq_uqps vcmpnge_usps vcmpngeps vcmpngt_usps vcmpngtps vcmpfalse_oqps vcmpfalseps vcmpneq_oqps vcmpge_osps vcmpgeps vcmpgt_osps vcmpgtps vcmptrue_uqps vcmptrueps vcmplt_oqps vcmple_oqps vcmpunord_sps vcmpneq_usps vcmpnlt_uqps vcmpnle_uqps vcmpord_sps vcmpeq_usps vcmpnge_uqps vcmpngt_uqps vcmpfalse_osps vcmpneq_osps vcmpge_oqps vcmpgt_oqps vcmptrue_usps vcmpps vcmpeq_ossd vcmpeqsd vcmplt_ossd vcmpltsd vcmple_ossd vcmplesd vcmpunord_qsd vcmpunordsd vcmpneq_uqsd vcmpneqsd vcmpnlt_ussd vcmpnltsd vcmpnle_ussd vcmpnlesd vcmpord_qsd vcmpordsd vcmpeq_uqsd vcmpnge_ussd vcmpngesd vcmpngt_ussd vcmpngtsd vcmpfalse_oqsd vcmpfalsesd vcmpneq_oqsd vcmpge_ossd vcmpgesd vcmpgt_ossd vcmpgtsd vcmptrue_uqsd vcmptruesd vcmplt_oqsd vcmple_oqsd vcmpunord_ssd vcmpneq_ussd vcmpnlt_uqsd vcmpnle_uqsd vcmpord_ssd vcmpeq_ussd vcmpnge_uqsd vcmpngt_uqsd vcmpfalse_ossd vcmpneq_ossd vcmpge_oqsd vcmpgt_oqsd vcmptrue_ussd vcmpsd vcmpeq_osss vcmpeqss vcmplt_osss vcmpltss vcmple_osss vcmpless vcmpunord_qss vcmpunordss vcmpneq_uqss vcmpneqss vcmpnlt_usss vcmpnltss vcmpnle_usss vcmpnless vcmpord_qss vcmpordss vcmpeq_uqss vcmpnge_usss vcmpngess vcmpngt_usss vcmpngtss vcmpfalse_oqss vcmpfalsess vcmpneq_oqss vcmpge_osss vcmpgess vcmpgt_osss vcmpgtss vcmptrue_uqss vcmptruess vcmplt_oqss vcmple_oqss vcmpunord_sss vcmpneq_usss vcmpnlt_uqss vcmpnle_uqss vcmpord_sss vcmpeq_usss vcmpnge_uqss vcmpngt_uqss vcmpfalse_osss vcmpneq_osss vcmpge_oqss vcmpgt_oqss vcmptrue_usss vcmpss vcomisd vcomiss vcvtdq2pd vcvtdq2ps vcvtpd2dq vcvtpd2ps vcvtps2dq vcvtps2pd vcvtsd2si vcvtsd2ss vcvtsi2sd vcvtsi2ss vcvtss2sd vcvtss2si vcvttpd2dq vcvttps2dq vcvttsd2si vcvttss2si vdivpd vdivps vdivsd vdivss vdppd vdpps vextractf128 vextractps vhaddpd vhaddps vhsubpd vhsubps vinsertf128 vinsertps vlddqu vldqqu vldmxcsr vmaskmovdqu vmaskmovps vmaskmovpd vmaxpd vmaxps vmaxsd vmaxss vminpd vminps vminsd vminss vmovapd vmovaps vmovd vmovq vmovddup vmovdqa vmovqqa vmovdqu vmovqqu vmovhlps vmovhpd vmovhps vmovlhps vmovlpd vmovlps vmovmskpd vmovmskps vmovntdq vmovntqq vmovntdqa vmovntpd vmovntps vmovsd vmovshdup vmovsldup vmovss vmovupd vmovups vmpsadbw vmulpd vmulps vmulsd vmulss vorpd vorps vpabsb vpabsw vpabsd vpacksswb vpackssdw vpackuswb vpackusdw vpaddb vpaddw vpaddd vpaddq vpaddsb vpaddsw vpaddusb vpaddusw vpalignr vpand vpandn vpavgb vpavgw vpblendvb vpblendw vpcmpestri vpcmpestrm vpcmpistri vpcmpistrm vpcmpeqb vpcmpeqw vpcmpeqd vpcmpeqq vpcmpgtb vpcmpgtw vpcmpgtd vpcmpgtq vpermilpd vpermilps vperm2f128 vpextrb vpextrw vpextrd vpextrq vphaddw vphaddd vphaddsw vphminposuw vphsubw vphsubd vphsubsw vpinsrb vpinsrw vpinsrd vpinsrq vpmaddwd vpmaddubsw vpmaxsb vpmaxsw vpmaxsd vpmaxub vpmaxuw vpmaxud vpminsb vpminsw vpminsd vpminub vpminuw vpminud vpmovmskb vpmovsxbw vpmovsxbd vpmovsxbq vpmovsxwd vpmovsxwq vpmovsxdq vpmovzxbw vpmovzxbd vpmovzxbq vpmovzxwd vpmovzxwq vpmovzxdq vpmulhuw vpmulhrsw vpmulhw vpmullw vpmulld vpmuludq vpmuldq vpor vpsadbw vpshufb vpshufd vpshufhw vpshuflw vpsignb vpsignw vpsignd vpslldq vpsrldq vpsllw vpslld vpsllq vpsraw vpsrad vpsrlw vpsrld vpsrlq vptest vpsubb vpsubw vpsubd vpsubq vpsubsb vpsubsw vpsubusb vpsubusw vpunpckhbw vpunpckhwd vpunpckhdq vpunpckhqdq vpunpcklbw vpunpcklwd vpunpckldq vpunpcklqdq vpxor vrcpps vrcpss vrsqrtps vrsqrtss vroundpd vroundps vroundsd vroundss vshufpd vshufps vsqrtpd vsqrtps vsqrtsd vsqrtss vstmxcsr vsubpd vsubps vsubsd vsubss vtestps vtestpd vucomisd vucomiss vunpckhpd vunpckhps vunpcklpd vunpcklps vxorpd vxorps vzeroall vzeroupper pclmullqlqdq pclmulhqlqdq pclmullqhqdq pclmulhqhqdq pclmulqdq vpclmullqlqdq vpclmulhqlqdq vpclmullqhqdq vpclmulhqhqdq vpclmulqdq vfmadd132ps vfmadd132pd vfmadd312ps vfmadd312pd vfmadd213ps vfmadd213pd vfmadd123ps vfmadd123pd vfmadd231ps vfmadd231pd vfmadd321ps vfmadd321pd vfmaddsub132ps vfmaddsub132pd vfmaddsub312ps vfmaddsub312pd vfmaddsub213ps vfmaddsub213pd vfmaddsub123ps vfmaddsub123pd vfmaddsub231ps vfmaddsub231pd vfmaddsub321ps vfmaddsub321pd vfmsub132ps vfmsub132pd vfmsub312ps vfmsub312pd vfmsub213ps vfmsub213pd vfmsub123ps vfmsub123pd vfmsub231ps vfmsub231pd vfmsub321ps vfmsub321pd vfmsubadd132ps vfmsubadd132pd vfmsubadd312ps vfmsubadd312pd vfmsubadd213ps vfmsubadd213pd vfmsubadd123ps vfmsubadd123pd vfmsubadd231ps vfmsubadd231pd vfmsubadd321ps vfmsubadd321pd vfnmadd132ps vfnmadd132pd vfnmadd312ps vfnmadd312pd vfnmadd213ps vfnmadd213pd vfnmadd123ps vfnmadd123pd vfnmadd231ps vfnmadd231pd vfnmadd321ps vfnmadd321pd vfnmsub132ps vfnmsub132pd vfnmsub312ps vfnmsub312pd vfnmsub213ps vfnmsub213pd vfnmsub123ps vfnmsub123pd vfnmsub231ps vfnmsub231pd vfnmsub321ps vfnmsub321pd vfmadd132ss vfmadd132sd vfmadd312ss vfmadd312sd vfmadd213ss vfmadd213sd vfmadd123ss vfmadd123sd vfmadd231ss vfmadd231sd vfmadd321ss vfmadd321sd vfmsub132ss vfmsub132sd vfmsub312ss vfmsub312sd vfmsub213ss vfmsub213sd vfmsub123ss vfmsub123sd vfmsub231ss vfmsub231sd vfmsub321ss vfmsub321sd vfnmadd132ss vfnmadd132sd vfnmadd312ss vfnmadd312sd vfnmadd213ss vfnmadd213sd vfnmadd123ss vfnmadd123sd vfnmadd231ss vfnmadd231sd vfnmadd321ss vfnmadd321sd vfnmsub132ss vfnmsub132sd vfnmsub312ss vfnmsub312sd vfnmsub213ss vfnmsub213sd vfnmsub123ss vfnmsub123sd vfnmsub231ss vfnmsub231sd vfnmsub321ss vfnmsub321sd rdfsbase rdgsbase rdrand wrfsbase wrgsbase vcvtph2ps vcvtps2ph adcx adox rdseed clac stac xstore xcryptecb xcryptcbc xcryptctr xcryptcfb xcryptofb montmul xsha1 xsha256 llwpcb slwpcb lwpval lwpins vfmaddpd vfmaddps vfmaddsd vfmaddss vfmaddsubpd vfmaddsubps vfmsubaddpd vfmsubaddps vfmsubpd vfmsubps vfmsubsd vfmsubss vfnmaddpd vfnmaddps vfnmaddsd vfnmaddss vfnmsubpd vfnmsubps vfnmsubsd vfnmsubss vfrczpd vfrczps vfrczsd vfrczss vpcmov vpcomb vpcomd vpcomq vpcomub vpcomud vpcomuq vpcomuw vpcomw vphaddbd vphaddbq vphaddbw vphadddq vphaddubd vphaddubq vphaddubw vphaddudq vphadduwd vphadduwq vphaddwd vphaddwq vphsubbw vphsubdq vphsubwd vpmacsdd vpmacsdqh vpmacsdql vpmacssdd vpmacssdqh vpmacssdql vpmacsswd vpmacssww vpmacswd vpmacsww vpmadcsswd vpmadcswd vpperm vprotb vprotd vprotq vprotw vpshab vpshad vpshaq vpshaw vpshlb vpshld vpshlq vpshlw vbroadcasti128 vpblendd vpbroadcastb vpbroadcastw vpbroadcastd vpbroadcastq vpermd vpermpd vpermps vpermq vperm2i128 vextracti128 vinserti128 vpmaskmovd vpmaskmovq vpsllvd vpsllvq vpsravd vpsrlvd vpsrlvq vgatherdpd vgatherqpd vgatherdps vgatherqps vpgatherdd vpgatherqd vpgatherdq vpgatherqq xabort xbegin xend xtest andn bextr blci blcic blsi blsic blcfill blsfill blcmsk blsmsk blsr blcs bzhi mulx pdep pext rorx sarx shlx shrx tzcnt tzmsk t1mskc valignd valignq vblendmpd vblendmps vbroadcastf32x4 vbroadcastf64x4 vbroadcasti32x4 vbroadcasti64x4 vcompresspd vcompressps vcvtpd2udq vcvtps2udq vcvtsd2usi vcvtss2usi vcvttpd2udq vcvttps2udq vcvttsd2usi vcvttss2usi vcvtudq2pd vcvtudq2ps vcvtusi2sd vcvtusi2ss vexpandpd vexpandps vextractf32x4 vextractf64x4 vextracti32x4 vextracti64x4 vfixupimmpd vfixupimmps vfixupimmsd vfixupimmss vgetexppd vgetexpps vgetexpsd vgetexpss vgetmantpd vgetmantps vgetmantsd vgetmantss vinsertf32x4 vinsertf64x4 vinserti32x4 vinserti64x4 vmovdqa32 vmovdqa64 vmovdqu32 vmovdqu64 vpabsq vpandd vpandnd vpandnq vpandq vpblendmd vpblendmq vpcmpltd vpcmpled vpcmpneqd vpcmpnltd vpcmpnled vpcmpd vpcmpltq vpcmpleq vpcmpneqq vpcmpnltq vpcmpnleq vpcmpq vpcmpequd vpcmpltud vpcmpleud vpcmpnequd vpcmpnltud vpcmpnleud vpcmpud vpcmpequq vpcmpltuq vpcmpleuq vpcmpnequq vpcmpnltuq vpcmpnleuq vpcmpuq vpcompressd vpcompressq vpermi2d vpermi2pd vpermi2ps vpermi2q vpermt2d vpermt2pd vpermt2ps vpermt2q vpexpandd vpexpandq vpmaxsq vpmaxuq vpminsq vpminuq vpmovdb vpmovdw vpmovqb vpmovqd vpmovqw vpmovsdb vpmovsdw vpmovsqb vpmovsqd vpmovsqw vpmovusdb vpmovusdw vpmovusqb vpmovusqd vpmovusqw vpord vporq vprold vprolq vprolvd vprolvq vprord vprorq vprorvd vprorvq vpscatterdd vpscatterdq vpscatterqd vpscatterqq vpsraq vpsravq vpternlogd vpternlogq vptestmd vptestmq vptestnmd vptestnmq vpxord vpxorq vrcp14pd vrcp14ps vrcp14sd vrcp14ss vrndscalepd vrndscaleps vrndscalesd vrndscaless vrsqrt14pd vrsqrt14ps vrsqrt14sd vrsqrt14ss vscalefpd vscalefps vscalefsd vscalefss vscatterdpd vscatterdps vscatterqpd vscatterqps vshuff32x4 vshuff64x2 vshufi32x4 vshufi64x2 kandnw kandw kmovw knotw kortestw korw kshiftlw kshiftrw kunpckbw kxnorw kxorw vpbroadcastmb2q vpbroadcastmw2d vpconflictd vpconflictq vplzcntd vplzcntq vexp2pd vexp2ps vrcp28pd vrcp28ps vrcp28sd vrcp28ss vrsqrt28pd vrsqrt28ps vrsqrt28sd vrsqrt28ss vgatherpf0dpd vgatherpf0dps vgatherpf0qpd vgatherpf0qps vgatherpf1dpd vgatherpf1dps vgatherpf1qpd vgatherpf1qps vscatterpf0dpd vscatterpf0dps vscatterpf0qpd vscatterpf0qps vscatterpf1dpd vscatterpf1dps vscatterpf1qpd vscatterpf1qps prefetchwt1 bndmk bndcl bndcu bndcn bndmov bndldx bndstx sha1rnds4 sha1nexte sha1msg1 sha1msg2 sha256rnds2 sha256msg1 sha256msg2 hint_nop0 hint_nop1 hint_nop2 hint_nop3 hint_nop4 hint_nop5 hint_nop6 hint_nop7 hint_nop8 hint_nop9 hint_nop10 hint_nop11 hint_nop12 hint_nop13 hint_nop14 hint_nop15 hint_nop16 hint_nop17 hint_nop18 hint_nop19 hint_nop20 hint_nop21 hint_nop22 hint_nop23 hint_nop24 hint_nop25 hint_nop26 hint_nop27 hint_nop28 hint_nop29 hint_nop30 hint_nop31 hint_nop32 hint_nop33 hint_nop34 hint_nop35 hint_nop36 hint_nop37 hint_nop38 hint_nop39 hint_nop40 hint_nop41 hint_nop42 hint_nop43 hint_nop44 hint_nop45 hint_nop46 hint_nop47 hint_nop48 hint_nop49 hint_nop50 hint_nop51 hint_nop52 hint_nop53 hint_nop54 hint_nop55 hint_nop56 hint_nop57 hint_nop58 hint_nop59 hint_nop60 hint_nop61 hint_nop62 hint_nop63",literal:"ip eip rip al ah bl bh cl ch dl dh sil dil bpl spl r8b r9b r10b r11b r12b r13b r14b r15b ax bx cx dx si di bp sp r8w r9w r10w r11w r12w r13w r14w r15w eax ebx ecx edx esi edi ebp esp eip r8d r9d r10d r11d r12d r13d r14d r15d rax rbx rcx rdx rsi rdi rbp rsp r8 r9 r10 r11 r12 r13 r14 r15 cs ds es fs gs ss st st0 st1 st2 st3 st4 st5 st6 st7 mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7 xmm0 xmm1 xmm2 xmm3 xmm4 xmm5 xmm6 xmm7 xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15 xmm16 xmm17 xmm18 xmm19 xmm20 xmm21 xmm22 xmm23 xmm24 xmm25 xmm26 xmm27 xmm28 xmm29 xmm30 xmm31 ymm0 ymm1 ymm2 ymm3 ymm4 ymm5 ymm6 ymm7 ymm8 ymm9 ymm10 ymm11 ymm12 ymm13 ymm14 ymm15 ymm16 ymm17 ymm18 ymm19 ymm20 ymm21 ymm22 ymm23 ymm24 ymm25 ymm26 ymm27 ymm28 ymm29 ymm30 ymm31 zmm0 zmm1 zmm2 zmm3 zmm4 zmm5 zmm6 zmm7 zmm8 zmm9 zmm10 zmm11 zmm12 zmm13 zmm14 zmm15 zmm16 zmm17 zmm18 zmm19 zmm20 zmm21 zmm22 zmm23 zmm24 zmm25 zmm26 zmm27 zmm28 zmm29 zmm30 zmm31 k0 k1 k2 k3 k4 k5 k6 k7 bnd0 bnd1 bnd2 bnd3 cr0 cr1 cr2 cr3 cr4 cr8 dr0 dr1 dr2 dr3 dr8 tr3 tr4 tr5 tr6 tr7 r0 r1 r2 r3 r4 r5 r6 r7 r0b r1b r2b r3b r4b r5b r6b r7b r0w r1w r2w r3w r4w r5w r6w r7w r0d r1d r2d r3d r4d r5d r6d r7d r0h r1h r2h r3h r0l r1l r2l r3l r4l r5l r6l r7l r8l r9l r10l r11l r12l r13l r14l r15l",pseudo:"db dw dd dq dt ddq do dy dz resb resw resd resq rest resdq reso resy resz incbin equ times",preprocessor:"%define %xdefine %+ %undef %defstr %deftok %assign %strcat %strlen %substr %rotate %elif %else %endif %ifmacro %ifctx %ifidn %ifidni %ifid %ifnum %ifstr %iftoken %ifempty %ifenv %error %warning %fatal %rep %endrep %include %push %pop %repl %pathsearch %depend %use %arg %stacksize %local %line %comment %endcomment .nolist byte word dword qword nosplit rel abs seg wrt strict near far a32 ptr __FILE__ __LINE__ __SECT__ __BITS__ __OUTPUT_FORMAT__ __DATE__ __TIME__ __DATE_NUM__ __TIME_NUM__ __UTC_DATE__ __UTC_TIME__ __UTC_DATE_NUM__ __UTC_TIME_NUM__ __PASS__ struc endstruc istruc at iend align alignb sectalign daz nodaz up down zero default option assume public ",built_in:"bits use16 use32 use64 default section segment absolute extern global common cpu float __utf16__ __utf16le__ __utf16be__ __utf32__ __utf32le__ __utf32be__ __float8__ __float16__ __float32__ __float64__ __float80m__ __float80e__ __float128l__ __float128h__ __Infinity__ __QNaN__ __SNaN__ Inf NaN QNaN SNaN float8 float16 float32 float64 float80m float80e float128l float128h __FLOAT_DAZ__ __FLOAT_ROUND__ __FLOAT__"},c:[{cN:"comment",b:";",e:"$",r:0},{cN:"number",b:"\\b(?:([0-9][0-9_]*)?\\.[0-9_]*(?:[eE][+-]?[0-9_]+)?|(0[Xx])?[0-9][0-9_]*\\.?[0-9_]*(?:[pP](?:[+-]?[0-9_]+)?)?)\\b",r:0},{cN:"number",b:"\\$[0-9][0-9A-Fa-f]*",r:0},{cN:"number",b:"\\b(?:[0-9A-Fa-f][0-9A-Fa-f_]*[HhXx]|[0-9][0-9_]*[DdTt]?|[0-7][0-7_]*[QqOo]|[0-1][0-1_]*[BbYy])\\b"},{cN:"number",b:"\\b(?:0[HhXx][0-9A-Fa-f_]+|0[DdTt][0-9_]+|0[QqOo][0-7_]+|0[BbYy][0-1_]+)\\b"},a.QSM,{cN:"string",b:"'",e:"[^\\\\]'",r:0},{cN:"string",b:"`",e:"[^\\\\]`",r:0},{cN:"string",b:"\\.[A-Za-z0-9]+",r:0},{cN:"label",b:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)",r:0},{cN:"label",b:"^\\s*%%[A-Za-z0-9_$#@~.?]*:",r:0},{cN:"argument",b:"%[0-9]+",r:0},{cN:"built_in",b:"%!S+",r:0}]}});
\ No newline at end of file diff --git a/vendor/assets/javascripts/jquery.sticky-kit.min.js b/vendor/assets/javascripts/jquery.sticky-kit.min.js new file mode 100644 index 00000000000..e8bb207c5a5 --- /dev/null +++ b/vendor/assets/javascripts/jquery.sticky-kit.min.js @@ -0,0 +1,9 @@ +/* + Sticky-kit v1.1.1 | WTFPL | Leaf Corcoran 2014 | http://leafo.net +*/ +(function(){var k,e;k=this.jQuery||window.jQuery;e=k(window);k.fn.stick_in_parent=function(d){var v,y,n,p,h,C,s,G,q,H;null==d&&(d={});s=d.sticky_class;y=d.inner_scrolling;C=d.recalc_every;h=d.parent;p=d.offset_top;n=d.spacer;v=d.bottoming;null==p&&(p=0);null==h&&(h=void 0);null==y&&(y=!0);null==s&&(s="is_stuck");null==v&&(v=!0);G=function(a,d,q,z,D,t,r,E){var u,F,m,A,c,f,B,w,x,g,b;if(!a.data("sticky_kit")){a.data("sticky_kit",!0);f=a.parent();null!=h&&(f=f.closest(h));if(!f.length)throw"failed to find stick parent"; +u=m=!1;(g=null!=n?n&&a.closest(n):k("<div />"))&&g.css("position",a.css("position"));B=function(){var c,e,l;if(!E&&(c=parseInt(f.css("border-top-width"),10),e=parseInt(f.css("padding-top"),10),d=parseInt(f.css("padding-bottom"),10),q=f.offset().top+c+e,z=f.height(),m&&(u=m=!1,null==n&&(a.insertAfter(g),g.detach()),a.css({position:"",top:"",width:"",bottom:""}).removeClass(s),l=!0),D=a.offset().top-parseInt(a.css("margin-top"),10)-p,t=a.outerHeight(!0),r=a.css("float"),g&&g.css({width:a.outerWidth(!0), +height:t,display:a.css("display"),"vertical-align":a.css("vertical-align"),"float":r}),l))return b()};B();if(t!==z)return A=void 0,c=p,x=C,b=function(){var b,k,l,h;if(!E&&(null!=x&&(--x,0>=x&&(x=C,B())),l=e.scrollTop(),null!=A&&(k=l-A),A=l,m?(v&&(h=l+t+c>z+q,u&&!h&&(u=!1,a.css({position:"fixed",bottom:"",top:c}).trigger("sticky_kit:unbottom"))),l<D&&(m=!1,c=p,null==n&&("left"!==r&&"right"!==r||a.insertAfter(g),g.detach()),b={position:"",width:"",top:""},a.css(b).removeClass(s).trigger("sticky_kit:unstick")), +y&&(b=e.height(),t+p>b&&!u&&(c-=k,c=Math.max(b-t,c),c=Math.min(p,c),m&&a.css({top:c+"px"})))):l>D&&(m=!0,b={position:"fixed",top:c},b.width="border-box"===a.css("box-sizing")?a.outerWidth()+"px":a.width()+"px",a.css(b).addClass(s),null==n&&(a.after(g),"left"!==r&&"right"!==r||g.append(a)),a.trigger("sticky_kit:stick")),m&&v&&(null==h&&(h=l+t+c>z+q),!u&&h)))return u=!0,"static"===f.css("position")&&f.css({position:"relative"}),a.css({position:"absolute",bottom:d,top:"auto"}).trigger("sticky_kit:bottom")}, +w=function(){B();return b()},F=function(){E=!0;e.off("touchmove",b);e.off("scroll",b);e.off("resize",w);k(document.body).off("sticky_kit:recalc",w);a.off("sticky_kit:detach",F);a.removeData("sticky_kit");a.css({position:"",bottom:"",top:"",width:""});f.position("position","");if(m)return null==n&&("left"!==r&&"right"!==r||a.insertAfter(g),g.remove()),a.removeClass(s)},e.on("touchmove",b),e.on("scroll",b),e.on("resize",w),k(document.body).on("sticky_kit:recalc",w),a.on("sticky_kit:detach",F),setTimeout(b, +0)}};q=0;for(H=this.length;q<H;q++)d=this[q],G(k(d));return this}}).call(this); diff --git a/vendor/assets/javascripts/pwstrength-bootstrap-1.2.2.js b/vendor/assets/javascripts/pwstrength-bootstrap-1.2.2.js deleted file mode 100644 index ee374a07fab..00000000000 --- a/vendor/assets/javascripts/pwstrength-bootstrap-1.2.2.js +++ /dev/null @@ -1,659 +0,0 @@ -/*! - * jQuery Password Strength plugin for Twitter Bootstrap - * - * Copyright (c) 2008-2013 Tane Piper - * Copyright (c) 2013 Alejandro Blanco - * Dual licensed under the MIT and GPL licenses. - */ - -(function (jQuery) { -// Source: src/rules.js - - var rulesEngine = {}; - - try { - if (!jQuery && module && module.exports) { - var jQuery = require("jquery"), - jsdom = require("jsdom").jsdom; - jQuery = jQuery(jsdom().parentWindow); - } - } catch (ignore) {} - - (function ($, rulesEngine) { - "use strict"; - var validation = {}; - - rulesEngine.forbiddenSequences = [ - "0123456789", "abcdefghijklmnopqrstuvwxyz", "qwertyuiop", "asdfghjkl", - "zxcvbnm", "!@#$%^&*()_+" - ]; - - validation.wordNotEmail = function (options, word, score) { - if (word.match(/^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i)) { - return score; - } - return 0; - }; - - validation.wordLength = function (options, word, score) { - var wordlen = word.length, - lenScore = Math.pow(wordlen, options.rules.raisePower); - if (wordlen < options.common.minChar) { - lenScore = (lenScore + score); - } - return lenScore; - }; - - validation.wordSimilarToUsername = function (options, word, score) { - var username = $(options.common.usernameField).val(); - if (username && word.toLowerCase().match(username.toLowerCase())) { - return score; - } - return 0; - }; - - validation.wordTwoCharacterClasses = function (options, word, score) { - if (word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) || - (word.match(/([a-zA-Z])/) && word.match(/([0-9])/)) || - (word.match(/(.[!,@,#,$,%,\^,&,*,?,_,~])/) && word.match(/[a-zA-Z0-9_]/))) { - return score; - } - return 0; - }; - - validation.wordRepetitions = function (options, word, score) { - if (word.match(/(.)\1\1/)) { return score; } - return 0; - }; - - validation.wordSequences = function (options, word, score) { - var found = false, - j; - if (word.length > 2) { - $.each(rulesEngine.forbiddenSequences, function (idx, seq) { - var sequences = [seq, seq.split('').reverse().join('')]; - $.each(sequences, function (idx, sequence) { - for (j = 0; j < (word.length - 2); j += 1) { // iterate the word trough a sliding window of size 3: - if (sequence.indexOf(word.toLowerCase().substring(j, j + 3)) > -1) { - found = true; - } - } - }); - }); - if (found) { return score; } - } - return 0; - }; - - validation.wordLowercase = function (options, word, score) { - return word.match(/[a-z]/) && score; - }; - - validation.wordUppercase = function (options, word, score) { - return word.match(/[A-Z]/) && score; - }; - - validation.wordOneNumber = function (options, word, score) { - return word.match(/\d+/) && score; - }; - - validation.wordThreeNumbers = function (options, word, score) { - return word.match(/(.*[0-9].*[0-9].*[0-9])/) && score; - }; - - validation.wordOneSpecialChar = function (options, word, score) { - return word.match(/.[!,@,#,$,%,\^,&,*,?,_,~]/) && score; - }; - - validation.wordTwoSpecialChar = function (options, word, score) { - return word.match(/(.*[!,@,#,$,%,\^,&,*,?,_,~].*[!,@,#,$,%,\^,&,*,?,_,~])/) && score; - }; - - validation.wordUpperLowerCombo = function (options, word, score) { - return word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) && score; - }; - - validation.wordLetterNumberCombo = function (options, word, score) { - return word.match(/([a-zA-Z])/) && word.match(/([0-9])/) && score; - }; - - validation.wordLetterNumberCharCombo = function (options, word, score) { - return word.match(/([a-zA-Z0-9].*[!,@,#,$,%,\^,&,*,?,_,~])|([!,@,#,$,%,\^,&,*,?,_,~].*[a-zA-Z0-9])/) && score; - }; - - rulesEngine.validation = validation; - - rulesEngine.executeRules = function (options, word) { - var totalScore = 0; - - $.each(options.rules.activated, function (rule, active) { - if (active) { - var score = options.rules.scores[rule], - funct = rulesEngine.validation[rule], - result, - errorMessage; - - if (!$.isFunction(funct)) { - funct = options.rules.extra[rule]; - } - - if ($.isFunction(funct)) { - result = funct(options, word, score); - if (result) { - totalScore += result; - } - if (result < 0 || (!$.isNumeric(result) && !result)) { - errorMessage = options.ui.spanError(options, rule); - if (errorMessage.length > 0) { - options.instances.errors.push(errorMessage); - } - } - } - } - }); - - return totalScore; - }; - }(jQuery, rulesEngine)); - - try { - if (module && module.exports) { - module.exports = rulesEngine; - } - } catch (ignore) {} - -// Source: src/options.js - - - - - var defaultOptions = {}; - - defaultOptions.common = {}; - defaultOptions.common.minChar = 6; - defaultOptions.common.usernameField = "#username"; - defaultOptions.common.userInputs = [ - // Selectors for input fields with user input - ]; - defaultOptions.common.onLoad = undefined; - defaultOptions.common.onKeyUp = undefined; - defaultOptions.common.zxcvbn = false; - defaultOptions.common.debug = false; - - defaultOptions.rules = {}; - defaultOptions.rules.extra = {}; - defaultOptions.rules.scores = { - wordNotEmail: -100, - wordLength: -50, - wordSimilarToUsername: -100, - wordSequences: -50, - wordTwoCharacterClasses: 2, - wordRepetitions: -25, - wordLowercase: 1, - wordUppercase: 3, - wordOneNumber: 3, - wordThreeNumbers: 5, - wordOneSpecialChar: 3, - wordTwoSpecialChar: 5, - wordUpperLowerCombo: 2, - wordLetterNumberCombo: 2, - wordLetterNumberCharCombo: 2 - }; - defaultOptions.rules.activated = { - wordNotEmail: true, - wordLength: true, - wordSimilarToUsername: true, - wordSequences: true, - wordTwoCharacterClasses: false, - wordRepetitions: false, - wordLowercase: true, - wordUppercase: true, - wordOneNumber: true, - wordThreeNumbers: true, - wordOneSpecialChar: true, - wordTwoSpecialChar: true, - wordUpperLowerCombo: true, - wordLetterNumberCombo: true, - wordLetterNumberCharCombo: true - }; - defaultOptions.rules.raisePower = 1.4; - - defaultOptions.ui = {}; - defaultOptions.ui.bootstrap2 = false; - defaultOptions.ui.showProgressBar = true; - defaultOptions.ui.showPopover = false; - defaultOptions.ui.showStatus = false; - defaultOptions.ui.spanError = function (options, key) { - "use strict"; - var text = options.ui.errorMessages[key]; - if (!text) { return ''; } - return '<span style="color: #d52929">' + text + '</span>'; - }; - defaultOptions.ui.errorMessages = { - wordLength: "Your password is too short", - wordNotEmail: "Do not use your email as your password", - wordSimilarToUsername: "Your password cannot contain your username", - wordTwoCharacterClasses: "Use different character classes", - wordRepetitions: "Too many repetitions", - wordSequences: "Your password contains sequences" - }; - defaultOptions.ui.verdicts = ["Weak", "Normal", "Medium", "Strong", "Very Strong"]; - defaultOptions.ui.showVerdicts = true; - defaultOptions.ui.showVerdictsInsideProgressBar = false; - defaultOptions.ui.showErrors = false; - defaultOptions.ui.container = undefined; - defaultOptions.ui.viewports = { - progress: undefined, - verdict: undefined, - errors: undefined - }; - defaultOptions.ui.scores = [14, 26, 38, 50]; - -// Source: src/ui.js - - - - - var ui = {}; - - (function ($, ui) { - "use strict"; - - var barClasses = ["danger", "warning", "success"], - statusClasses = ["error", "warning", "success"]; - - ui.getContainer = function (options, $el) { - var $container; - - $container = $(options.ui.container); - if (!($container && $container.length === 1)) { - $container = $el.parent(); - } - return $container; - }; - - ui.findElement = function ($container, viewport, cssSelector) { - if (viewport) { - return $container.find(viewport).find(cssSelector); - } - return $container.find(cssSelector); - }; - - ui.getUIElements = function (options, $el) { - var $container, result; - - if (options.instances.viewports) { - return options.instances.viewports; - } - - $container = ui.getContainer(options, $el); - - result = {}; - result.$progressbar = ui.findElement($container, options.ui.viewports.progress, "div.progress"); - if (options.ui.showVerdictsInsideProgressBar) { - result.$verdict = result.$progressbar.find("span.password-verdict"); - } - - if (!options.ui.showPopover) { - if (!options.ui.showVerdictsInsideProgressBar) { - result.$verdict = ui.findElement($container, options.ui.viewports.verdict, "span.password-verdict"); - } - result.$errors = ui.findElement($container, options.ui.viewports.errors, "ul.error-list"); - } - - options.instances.viewports = result; - return result; - }; - - ui.initProgressBar = function (options, $el) { - var $container = ui.getContainer(options, $el), - progressbar = "<div class='progress'><div class='"; - - if (!options.ui.bootstrap2) { - progressbar += "progress-"; - } - progressbar += "bar'>"; - if (options.ui.showVerdictsInsideProgressBar) { - progressbar += "<span class='password-verdict'></span>"; - } - progressbar += "</div></div>"; - - if (options.ui.viewports.progress) { - $container.find(options.ui.viewports.progress).append(progressbar); - } else { - $(progressbar).insertAfter($el); - } - }; - - ui.initHelper = function (options, $el, html, viewport) { - var $container = ui.getContainer(options, $el); - if (viewport) { - $container.find(viewport).append(html); - } else { - $(html).insertAfter($el); - } - }; - - ui.initVerdict = function (options, $el) { - ui.initHelper(options, $el, "<span class='password-verdict'></span>", - options.ui.viewports.verdict); - }; - - ui.initErrorList = function (options, $el) { - ui.initHelper(options, $el, "<ul class='error-list'></ul>", - options.ui.viewports.errors); - }; - - ui.initPopover = function (options, $el) { - $el.popover("destroy"); - $el.popover({ - html: true, - placement: "top", - trigger: "manual", - content: " " - }); - }; - - ui.initUI = function (options, $el) { - if (options.ui.showPopover) { - ui.initPopover(options, $el); - } else { - if (options.ui.showErrors) { ui.initErrorList(options, $el); } - if (options.ui.showVerdicts && !options.ui.showVerdictsInsideProgressBar) { - ui.initVerdict(options, $el); - } - } - if (options.ui.showProgressBar) { - ui.initProgressBar(options, $el); - } - }; - - ui.possibleProgressBarClasses = ["danger", "warning", "success"]; - - ui.updateProgressBar = function (options, $el, cssClass, percentage) { - var $progressbar = ui.getUIElements(options, $el).$progressbar, - $bar = $progressbar.find(".progress-bar"), - cssPrefix = "progress-"; - - if (options.ui.bootstrap2) { - $bar = $progressbar.find(".bar"); - cssPrefix = ""; - } - - $.each(ui.possibleProgressBarClasses, function (idx, value) { - $bar.removeClass(cssPrefix + "bar-" + value); - }); - $bar.addClass(cssPrefix + "bar-" + barClasses[cssClass]); - $bar.css("width", percentage + '%'); - }; - - ui.updateVerdict = function (options, $el, text) { - var $verdict = ui.getUIElements(options, $el).$verdict; - $verdict.text(text); - }; - - ui.updateErrors = function (options, $el) { - var $errors = ui.getUIElements(options, $el).$errors, - html = ""; - $.each(options.instances.errors, function (idx, err) { - html += "<li>" + err + "</li>"; - }); - $errors.html(html); - }; - - ui.updatePopover = function (options, $el, verdictText) { - var popover = $el.data("bs.popover"), - html = "", - hide = true; - - if (options.ui.showVerdicts && - !options.ui.showVerdictsInsideProgressBar && - verdictText.length > 0) { - html = "<h5><span class='password-verdict'>" + verdictText + - "</span></h5>"; - hide = false; - } - if (options.ui.showErrors) { - html += "<div><ul class='error-list' style='margin-bottom: 0; margin-left: -20px'>"; - $.each(options.instances.errors, function (idx, err) { - html += "<li>" + err + "</li>"; - hide = false; - }); - html += "</ul></div>"; - } - - if (hide) { - $el.popover("hide"); - return; - } - - if (options.ui.bootstrap2) { popover = $el.data("popover"); } - - if (popover.$arrow && popover.$arrow.parents("body").length > 0) { - $el.find("+ .popover .popover-content").html(html); - } else { - // It's hidden - popover.options.content = html; - $el.popover("show"); - } - }; - - ui.updateFieldStatus = function (options, $el, cssClass) { - var targetClass = options.ui.bootstrap2 ? ".control-group" : ".form-group", - $container = $el.parents(targetClass).first(); - - $.each(statusClasses, function (idx, css) { - if (!options.ui.bootstrap2) { css = "has-" + css; } - $container.removeClass(css); - }); - - cssClass = statusClasses[cssClass]; - if (!options.ui.bootstrap2) { cssClass = "has-" + cssClass; } - $container.addClass(cssClass); - }; - - ui.percentage = function (score, maximun) { - var result = Math.floor(100 * score / maximun); - result = result < 0 ? 0 : result; - result = result > 100 ? 100 : result; - return result; - }; - - ui.getVerdictAndCssClass = function (options, score) { - var cssClass, verdictText, level; - - if (score <= 0) { - cssClass = 0; - level = -1; - verdictText = options.ui.verdicts[0]; - } else if (score < options.ui.scores[0]) { - cssClass = 0; - level = 0; - verdictText = options.ui.verdicts[0]; - } else if (score < options.ui.scores[1]) { - cssClass = 0; - level = 1; - verdictText = options.ui.verdicts[1]; - } else if (score < options.ui.scores[2]) { - cssClass = 1; - level = 2; - verdictText = options.ui.verdicts[2]; - } else if (score < options.ui.scores[3]) { - cssClass = 1; - level = 3; - verdictText = options.ui.verdicts[3]; - } else { - cssClass = 2; - level = 4; - verdictText = options.ui.verdicts[4]; - } - - return [verdictText, cssClass, level]; - }; - - ui.updateUI = function (options, $el, score) { - var cssClass, barPercentage, verdictText; - - cssClass = ui.getVerdictAndCssClass(options, score); - verdictText = cssClass[0]; - cssClass = cssClass[1]; - - if (options.ui.showProgressBar) { - barPercentage = ui.percentage(score, options.ui.scores[3]); - ui.updateProgressBar(options, $el, cssClass, barPercentage); - if (options.ui.showVerdictsInsideProgressBar) { - ui.updateVerdict(options, $el, verdictText); - } - } - - if (options.ui.showStatus) { - ui.updateFieldStatus(options, $el, cssClass); - } - - if (options.ui.showPopover) { - ui.updatePopover(options, $el, verdictText); - } else { - if (options.ui.showVerdicts && !options.ui.showVerdictsInsideProgressBar) { - ui.updateVerdict(options, $el, verdictText); - } - if (options.ui.showErrors) { - ui.updateErrors(options, $el); - } - } - }; - }(jQuery, ui)); - -// Source: src/methods.js - - - - - var methods = {}; - - (function ($, methods) { - "use strict"; - var onKeyUp, applyToAll; - - onKeyUp = function (event) { - var $el = $(event.target), - options = $el.data("pwstrength-bootstrap"), - word = $el.val(), - userInputs, - verdictText, - verdictLevel, - score; - - if (options === undefined) { return; } - - options.instances.errors = []; - if (options.common.zxcvbn) { - userInputs = []; - $.each(options.common.userInputs, function (idx, selector) { - userInputs.push($(selector).val()); - }); - userInputs.push($(options.common.usernameField).val()); - score = zxcvbn(word, userInputs).entropy; - } else { - score = rulesEngine.executeRules(options, word); - } - ui.updateUI(options, $el, score); - verdictText = ui.getVerdictAndCssClass(options, score); - verdictLevel = verdictText[2]; - verdictText = verdictText[0]; - - if (options.common.debug) { console.log(score + ' - ' + verdictText); } - - if ($.isFunction(options.common.onKeyUp)) { - options.common.onKeyUp(event, { - score: score, - verdictText: verdictText, - verdictLevel: verdictLevel - }); - } - }; - - methods.init = function (settings) { - this.each(function (idx, el) { - // Make it deep extend (first param) so it extends too the - // rules and other inside objects - var clonedDefaults = $.extend(true, {}, defaultOptions), - localOptions = $.extend(true, clonedDefaults, settings), - $el = $(el); - - localOptions.instances = {}; - $el.data("pwstrength-bootstrap", localOptions); - $el.on("keyup", onKeyUp); - $el.on("change", onKeyUp); - $el.on("onpaste", onKeyUp); - - ui.initUI(localOptions, $el); - if ($.trim($el.val())) { // Not empty, calculate the strength - $el.trigger("keyup"); - } - - if ($.isFunction(localOptions.common.onLoad)) { - localOptions.common.onLoad(); - } - }); - - return this; - }; - - methods.destroy = function () { - this.each(function (idx, el) { - var $el = $(el), - options = $el.data("pwstrength-bootstrap"), - elements = ui.getUIElements(options, $el); - elements.$progressbar.remove(); - elements.$verdict.remove(); - elements.$errors.remove(); - $el.removeData("pwstrength-bootstrap"); - }); - }; - - methods.forceUpdate = function () { - this.each(function (idx, el) { - var event = { target: el }; - onKeyUp(event); - }); - }; - - methods.addRule = function (name, method, score, active) { - this.each(function (idx, el) { - var options = $(el).data("pwstrength-bootstrap"); - - options.rules.activated[name] = active; - options.rules.scores[name] = score; - options.rules.extra[name] = method; - }); - }; - - applyToAll = function (rule, prop, value) { - this.each(function (idx, el) { - $(el).data("pwstrength-bootstrap").rules[prop][rule] = value; - }); - }; - - methods.changeScore = function (rule, score) { - applyToAll.call(this, rule, "scores", score); - }; - - methods.ruleActive = function (rule, active) { - applyToAll.call(this, rule, "activated", active); - }; - - $.fn.pwstrength = function (method) { - var result; - - if (methods[method]) { - result = methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); - } else if (typeof method === "object" || !method) { - result = methods.init.apply(this, arguments); - } else { - $.error("Method " + method + " does not exist on jQuery.pwstrength-bootstrap"); - } - - return result; - }; - }(jQuery, methods)); -}(jQuery));
\ No newline at end of file diff --git a/vendor/assets/stylesheets/highlightjs.min.css b/vendor/assets/stylesheets/highlightjs.min.css deleted file mode 100644 index f2429be6228..00000000000 --- a/vendor/assets/stylesheets/highlightjs.min.css +++ /dev/null @@ -1 +0,0 @@ -.hljs{display:block;padding:.5em;background:#f0f0f0}.hljs,.hljs-subst,.hljs-tag .hljs-title,.lisp .hljs-title,.clojure .hljs-built_in,.nginx .hljs-title{color:black}.hljs-string,.hljs-title,.hljs-constant,.hljs-parent,.hljs-tag .hljs-value,.hljs-rules .hljs-value,.hljs-rules .hljs-value .hljs-number,.hljs-preprocessor,.hljs-pragma,.haml .hljs-symbol,.ruby .hljs-symbol,.ruby .hljs-symbol .hljs-string,.hljs-aggregate,.hljs-template_tag,.django .hljs-variable,.smalltalk .hljs-class,.hljs-addition,.hljs-flow,.hljs-stream,.bash .hljs-variable,.apache .hljs-tag,.apache .hljs-cbracket,.tex .hljs-command,.tex .hljs-special,.erlang_repl .hljs-function_or_atom,.asciidoc .hljs-header,.markdown .hljs-header,.coffeescript .hljs-attribute{color:#800}.smartquote,.hljs-comment,.hljs-annotation,.hljs-template_comment,.diff .hljs-header,.hljs-chunk,.asciidoc .hljs-blockquote,.markdown .hljs-blockquote{color:#888}.hljs-number,.hljs-date,.hljs-regexp,.hljs-literal,.hljs-hexcolor,.smalltalk .hljs-symbol,.smalltalk .hljs-char,.go .hljs-constant,.hljs-change,.lasso .hljs-variable,.makefile .hljs-variable,.asciidoc .hljs-bullet,.markdown .hljs-bullet,.asciidoc .hljs-link_url,.markdown .hljs-link_url{color:#080}.hljs-label,.hljs-javadoc,.ruby .hljs-string,.hljs-decorator,.hljs-filter .hljs-argument,.hljs-localvars,.hljs-array,.hljs-attr_selector,.hljs-important,.hljs-pseudo,.hljs-pi,.haml .hljs-bullet,.hljs-doctype,.hljs-deletion,.hljs-envvar,.hljs-shebang,.apache .hljs-sqbracket,.nginx .hljs-built_in,.tex .hljs-formula,.erlang_repl .hljs-reserved,.hljs-prompt,.asciidoc .hljs-link_label,.markdown .hljs-link_label,.vhdl .hljs-attribute,.clojure .hljs-attribute,.asciidoc .hljs-attribute,.lasso .hljs-attribute,.coffeescript .hljs-property,.hljs-phony{color:#88F}.hljs-keyword,.hljs-id,.hljs-title,.hljs-built_in,.hljs-aggregate,.css .hljs-tag,.hljs-javadoctag,.hljs-phpdoc,.hljs-yardoctag,.smalltalk .hljs-class,.hljs-winutils,.bash .hljs-variable,.apache .hljs-tag,.go .hljs-typename,.tex .hljs-command,.asciidoc .hljs-strong,.markdown .hljs-strong,.hljs-request,.hljs-status{font-weight:bold}.asciidoc .hljs-emphasis,.markdown .hljs-emphasis{font-style:italic}.nginx .hljs-built_in{font-weight:normal}.coffeescript .javascript,.javascript .xml,.lasso .markup,.tex .hljs-formula,.xml .javascript,.xml .vbscript,.xml .css,.xml .hljs-cdata{opacity:.5}
\ No newline at end of file diff --git a/vendor/plugins/.gitkeep b/vendor/plugins/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 --- a/vendor/plugins/.gitkeep +++ /dev/null |