diff options
371 files changed, 5728 insertions, 3526 deletions
diff --git a/.hound.yml b/.hound.yml deleted file mode 100644 index 3bde29fb2bf..00000000000 --- a/.hound.yml +++ /dev/null @@ -1,4 +0,0 @@ -# Prefer single quotes -StringLiterals: - EnforcedStyle: single_quotes - Enabled: true diff --git a/.rubocop.yml b/.rubocop.yml index 3aac8401848..db0bcfadcf4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,6 +2,8 @@ require: - rubocop-rspec - ./rubocop/rubocop +inherit_from: .rubocop_todo.yml + AllCops: TargetRubyVersion: 2.1 # Cop names are not displayed in offense messages by default. Change behavior @@ -52,14 +54,6 @@ Style/AlignArray: Style/AlignHash: Enabled: true -# Align the parameters of a method call if they span more than one line. -Style/AlignParameters: - Enabled: false - -# Use &&/|| instead of and/or. -Style/AndOr: - Enabled: false - # Use `Array#join` instead of `Array#*`. Style/ArrayJoin: Enabled: true @@ -80,10 +74,6 @@ Style/Attr: Style/BeginBlock: Enabled: true -# Checks if usage of %() or %Q() matches configuration. -Style/BarePercentLiterals: - Enabled: false - # Do not use block comments. Style/BlockComments: Enabled: true @@ -97,14 +87,6 @@ Style/BlockEndNewline: Style/BlockDelimiters: Enabled: true -# Enforce braces style around hash parameters. -Style/BracesAroundHashParameters: - Enabled: false - -# Avoid explicit use of the case equality operator(===). -Style/CaseEquality: - Enabled: false - # Indentation of when in a case/when/[else/]end. Style/CaseIndentation: Enabled: true @@ -133,24 +115,10 @@ Style/ClassMethods: Style/ClassVars: Enabled: true -# Do not use :: for method call. -Style/ColonMethodCall: - Enabled: false - -# Checks formatting of special comments (TODO, FIXME, OPTIMIZE, HACK, REVIEW). -Style/CommentAnnotation: - Enabled: false - # Indentation of comments. Style/CommentIndentation: Enabled: true -# Use the return value of `if` and `case` statements for assignment to a -# variable and variable comparison instead of assigning that variable -# inside of each branch. -Style/ConditionalAssignment: - Enabled: false - # Constants should use SCREAMING_SNAKE_CASE. Style/ConstantName: Enabled: true @@ -159,34 +127,14 @@ Style/ConstantName: Style/DefWithParentheses: Enabled: true -# Checks for use of deprecated Hash methods. -Style/DeprecatedHashMethods: - Enabled: false - # Document classes and non-namespace modules. Style/Documentation: Enabled: false -# Checks the position of the dot in multi-line method calls. -Style/DotPosition: - Enabled: false - -# Checks for uses of double negation (!!). -Style/DoubleNegation: - Enabled: false - -# Prefer `each_with_object` over `inject` or `reduce`. -Style/EachWithObject: - Enabled: false - # Align elses and elsifs correctly. Style/ElseAlignment: Enabled: true -# Avoid empty else-clauses. -Style/EmptyElse: - Enabled: false - # Use empty lines between defs. Style/EmptyLineBetweenDefs: Enabled: false @@ -215,10 +163,6 @@ Style/EmptyLinesAroundModuleBody: Style/EmptyLinesAroundMethodBody: Enabled: false -# Prefer literals to Array.new/Hash.new/String.new. -Style/EmptyLiteral: - Enabled: false - # Avoid the use of END blocks. Style/EndBlock: Enabled: true @@ -231,10 +175,6 @@ Style/EndOfLine: Style/EvenOdd: Enabled: true -# Do not use unnecessary spacing. -Style/ExtraSpacing: - Enabled: false - # Use snake_case for source file names. Style/FileName: Enabled: true @@ -252,31 +192,15 @@ Style/FlipFlop: Style/For: Enabled: true -# Enforce the use of Kernel#sprintf, Kernel#format or String#%. -Style/FormatString: - Enabled: false - # Do not introduce global variables. Style/GlobalVars: Enabled: true -# Check for conditionals that can be replaced with guard clauses. -Style/GuardClause: - Enabled: false - # Prefer Ruby 1.9 hash syntax `{ a: 1, b: 2 }` # over 1.8 syntax `{ :a => 1, :b => 2 }`. Style/HashSyntax: Enabled: true -# Finds if nodes inside else, which can be converted to elsif. -Style/IfInsideElse: - Enabled: false - -# Favor modifier if/unless usage when you have a single-line body. -Style/IfUnlessModifier: - Enabled: false - # Do not use if x; .... Use the ternary operator instead. Style/IfWithSemicolon: Enabled: true @@ -299,22 +223,10 @@ Style/IndentationConsistency: Style/IndentationWidth: Enabled: true -# Checks the indentation of the first element in an array literal. -Style/IndentArray: - Enabled: false - -# Checks the indentation of the first key in a hash literal. -Style/IndentHash: - Enabled: false - # Use Kernel#loop for infinite loops. Style/InfiniteLoop: Enabled: true -# Use the new lambda literal syntax for single-line blocks. -Style/Lambda: - Enabled: false - # Use lambda.call(...) instead of lambda.(...). Style/LambdaCall: Enabled: true @@ -323,14 +235,6 @@ Style/LambdaCall: Style/LeadingCommentSpace: Enabled: true -# Use \ instead of + or << to concatenate two string literals at line end. -Style/LineEndConcatenation: - Enabled: false - -# Do not use parentheses for method calls with no arguments. -Style/MethodCallParentheses: - Enabled: false - # Checks if the method definitions have or don't have parentheses. Style/MethodDefParentheses: Enabled: true @@ -387,39 +291,18 @@ Style/MultilineMethodDefinitionBraceLayout: Style/MultilineOperationIndentation: Enabled: false -# Avoid multi-line `? :` (the ternary operator), use if/unless instead. -Style/MultilineTernaryOperator: - Enabled: false - -# Do not assign mutable objects to constants. -Style/MutableConstant: - Enabled: false - # Favor unless over if for negative conditions (or control flow or). Style/NegatedIf: Enabled: true -# Favor until over while for negative conditions. -Style/NegatedWhile: - Enabled: false - # Avoid using nested modifiers. Style/NestedModifier: Enabled: true -# Parenthesize method calls which are nested inside the argument list of -# another parenthesized method call. -Style/NestedParenthesizedCalls: - Enabled: false - # Use one expression per branch in a ternary operator. Style/NestedTernaryOperator: Enabled: true -# Use `next` to skip iteration instead of a condition at the end. -Style/Next: - Enabled: false - # Prefer x.nil? to x == nil. Style/NilComparison: Enabled: true @@ -444,51 +327,10 @@ Style/OneLineConditional: Style/OpMethod: Enabled: true -# Check for simple usages of parallel assignment. It will only warn when -# the number of variables matches on both sides of the assignment. -Style/ParallelAssignment: - Enabled: false - # Don't use parentheses around the condition of an if/unless/while. Style/ParenthesesAroundCondition: Enabled: true -# Use `%`-literal delimiters consistently. -Style/PercentLiteralDelimiters: - Enabled: false - -# Checks if uses of %Q/%q match the configured preference. -Style/PercentQLiterals: - Enabled: false - -# Avoid Perl-style regex back references. -Style/PerlBackrefs: - Enabled: false - -# Check the names of predicate methods. -Style/PredicateName: - Enabled: false - -# Use proc instead of Proc.new. -Style/Proc: - Enabled: false - -# Checks the arguments passed to raise/fail. -Style/RaiseArgs: - Enabled: false - -# Don't use begin blocks when they are not needed. -Style/RedundantBegin: - Enabled: false - -# Checks for an obsolete RuntimeException argument in raise/fail. -Style/RedundantException: - Enabled: false - -# Checks usages of Object#freeze on immutable objects. -Style/RedundantFreeze: - Enabled: false - # Checks for parentheses that seem not to serve any purpose. Style/RedundantParentheses: Enabled: true @@ -497,24 +339,6 @@ Style/RedundantParentheses: Style/RedundantReturn: Enabled: true -# Don't use self where it's not needed. -Style/RedundantSelf: - Enabled: false - -# Use %r for regular expressions matching more than `MaxSlashes` '/' -# characters. Use %r only for regular expressions matching more -# than `MaxSlashes` '/' character. -Style/RegexpLiteral: - Enabled: false - -# Avoid using rescue in its modifier form. -Style/RescueModifier: - Enabled: false - -# Checks for places where self-assignment shorthand should have been used. -Style/SelfAssignment: - Enabled: false - # Don't use semicolons to terminate expressions. Style/Semicolon: Enabled: true @@ -524,14 +348,6 @@ Style/SignalException: EnforcedStyle: only_raise Enabled: true -# Enforces the names of some block params. -Style/SingleLineBlockParams: - Enabled: false - -# Avoid single-line methods. -Style/SingleLineMethods: - Enabled: false - # Use spaces after colons. Style/SpaceAfterColon: Enabled: true @@ -553,11 +369,6 @@ Style/SpaceAfterNot: Style/SpaceAfterSemicolon: Enabled: true -# Checks that the equals signs in parameter default assignments have or don't -# have surrounding space depending on configuration. -Style/SpaceAroundEqualsInParameterDefault: - Enabled: false - # Use a space around keywords if appropriate. Style/SpaceAroundKeyword: Enabled: true @@ -566,10 +377,6 @@ Style/SpaceAroundKeyword: Style/SpaceAroundOperators: Enabled: true -# Checks that the left block brace has or doesn't have space before it. -Style/SpaceBeforeBlockBraces: - Enabled: false - # No spaces before commas. Style/SpaceBeforeComma: Enabled: true @@ -578,33 +385,14 @@ Style/SpaceBeforeComma: Style/SpaceBeforeComment: Enabled: true -# Checks that exactly one space is used between a method name and the first -# argument for method calls without parentheses. -Style/SpaceBeforeFirstArg: - Enabled: false - # No spaces before semicolons. Style/SpaceBeforeSemicolon: Enabled: true -# Checks that block braces have or don't have surrounding space. -# For blocks taking parameters, checks that the left brace has or doesn't -# have trailing space. -Style/SpaceInsideBlockBraces: - Enabled: false - -# No spaces after [ or before ]. -Style/SpaceInsideBrackets: - Enabled: false - # Use spaces inside hash literal braces - or don't. Style/SpaceInsideHashLiteralBraces: Enabled: true -# No spaces after ( or before ). -Style/SpaceInsideParens: - Enabled: false - # No spaces inside range literals. Style/SpaceInsideRangeLiteral: Enabled: true @@ -614,10 +402,6 @@ Style/SpaceInsideStringInterpolation: EnforcedStyle: no_space Enabled: true -# Avoid Perl-style global variables. -Style/SpecialGlobalVars: - Enabled: false - # Check for the usage of parentheses around stabby lambda arguments. Style/StabbyLambdaParentheses: EnforcedStyle: require_parentheses @@ -627,25 +411,12 @@ Style/StabbyLambdaParentheses: Style/StringLiterals: Enabled: false -# Checks if uses of quotes inside expressions in interpolated strings match the -# configured preference. -Style/StringLiteralsInInterpolation: - Enabled: false - # Checks if configured preferred methods are used over non-preferred. Style/StringMethods: PreferredMethods: intern: to_sym Enabled: true -# Use %i or %I for arrays of symbols. -Style/SymbolArray: - Enabled: false - -# Use symbols as procs instead of blocks when possible. -Style/SymbolProc: - Enabled: false - # No hard tabs. Style/Tab: Enabled: true @@ -654,40 +425,10 @@ Style/Tab: Style/TrailingBlankLines: Enabled: true -# Checks for trailing comma in array and hash literals. -Style/TrailingCommaInLiteral: - Enabled: false - -# Checks for trailing comma in argument lists. -Style/TrailingCommaInArguments: - Enabled: false - -# Avoid trailing whitespace. -Style/TrailingWhitespace: - Enabled: false - -# Checks for the usage of unneeded trailing underscores at the end of -# parallel variable assignment. -Style/TrailingUnderscoreVariable: - Enabled: false - -# Prefer attr_* methods to trivial readers/writers. -Style/TrivialAccessors: - Enabled: false - -# Do not use unless with else. Rewrite these with the positive case first. -Style/UnlessElse: - Enabled: false - # Checks for %W when interpolation is not needed. Style/UnneededCapitalW: Enabled: true -# TODO: Enable UnneededInterpolation Cop. -# Checks for strings that are just an interpolated expression. -Style/UnneededInterpolation: - Enabled: false - # Checks for %q/%Q when single quotes or double quotes would do. Style/UnneededPercentQ: Enabled: false @@ -717,12 +458,6 @@ Style/WhileUntilModifier: Style/WordArray: Enabled: false -# TODO: Enable ZeroLengthPredicate Cop. -# Use #empty? when testing for objects of length 0. -Style/ZeroLengthPredicate: - Enabled: false - - #################### Metrics ################################ # A calculated magnitude based on number of assignments, @@ -776,15 +511,6 @@ Metrics/PerceivedComplexity: Lint/AmbiguousOperator: Enabled: true -# Checks for ambiguous regexp literals in the first argument of a method -# invocation without parentheses. -Lint/AmbiguousRegexpLiteral: - Enabled: false - -# Don't use assignment in conditions. -Lint/AssignmentInCondition: - Enabled: false - # Align block ends correctly. Lint/BlockAlignment: Enabled: true @@ -810,14 +536,6 @@ Lint/DefEndAlignment: Lint/DeprecatedClassMethods: Enabled: true -# Check for duplicate method definitions. -Lint/DuplicateMethods: - Enabled: false - -# Check for duplicate keys in hash literals. -Lint/DuplicatedKey: - Enabled: false - # Check for immutable argument given to each_with_object. Lint/EachWithObjectArgument: Enabled: true @@ -830,10 +548,6 @@ Lint/ElseLayout: Lint/EmptyEnsure: Enabled: true -# Checks for empty string interpolation. -Lint/EmptyInterpolation: - Enabled: false - # Align ends correctly. Lint/EndAlignment: Enabled: true @@ -858,21 +572,11 @@ Lint/FloatOutOfRange: Lint/FormatParameterMismatch: Enabled: true -# Don't suppress exception. -Lint/HandleExceptions: - Enabled: false - # Checks for adjacent string literals on the same line, which could better be # represented as a single string literal. Lint/ImplicitStringConcatenation: Enabled: true -# TODO: Enable IneffectiveAccessModifier Cop. -# Checks for attempts to use `private` or `protected` to set the visibility -# of a class method, which does not work. -Lint/IneffectiveAccessModifier: - Enabled: false - # Checks for invalid character literals with a non-escaped whitespace # character. Lint/InvalidCharacterLiteral: @@ -886,11 +590,6 @@ Lint/LiteralInCondition: Lint/LiteralInInterpolation: Enabled: true -# Use Kernel#loop with break rather than begin/end/until or begin/end/while -# for post-loop tests. -Lint/Loop: - Enabled: false - # Do not use nested method definitions. Lint/NestedMethodDefinition: Enabled: true @@ -916,13 +615,8 @@ Lint/RequireParentheses: Lint/RescueException: Enabled: true -# Do not use the same name as outer local variable for block arguments -# or block local variables. -Lint/ShadowingOuterLocalVariable: - Enabled: false - -# 'Checks for Object#to_s usage in string interpolation. -Lint/StringConversionInInterpolation: +# Checks for the order which exceptions are rescued to avoid rescueing a less specific exception before a more specific exception. +Lint/ShadowedException: Enabled: false # Do not use prefix `_` for a variable that is used. @@ -935,22 +629,10 @@ Lint/UnderscorePrefixedVariableName: Lint/UnneededDisable: Enabled: false -# Checks for unused block arguments. -Lint/UnusedBlockArgument: - Enabled: false - -# Checks for unused method arguments. -Lint/UnusedMethodArgument: - Enabled: false - # Unreachable code. Lint/UnreachableCode: Enabled: true -# Checks for useless access modifiers. -Lint/UselessAccessModifier: - Enabled: false - # Checks for useless assignment to a local variable. Lint/UselessAssignment: Enabled: true @@ -983,11 +665,6 @@ Performance/Casecmp: Performance/DoubleStartEndWith: Enabled: true -# TODO: Enable EndWith Cop. -# Use `end_with?` instead of a regex match anchored to the end of a string. -Performance/EndWith: - Enabled: false - # Use `strip` instead of `lstrip.rstrip`. Performance/LstripRstrip: Enabled: true @@ -996,24 +673,6 @@ Performance/LstripRstrip: Performance/RangeInclude: Enabled: true -# TODO: Enable RedundantBlockCall Cop. -# Use `yield` instead of `block.call`. -Performance/RedundantBlockCall: - Enabled: false - -# TODO: Enable RedundantMatch Cop. -# Use `=~` instead of `String#match` or `Regexp#match` in a context where the -# returned `MatchData` is not needed. -Performance/RedundantMatch: - Enabled: false - -# TODO: Enable RedundantMerge Cop. -# Use `Hash#[]=`, rather than `Hash#merge!` with a single key-value pair. -Performance/RedundantMerge: - # Max number of key-value pairs to consider an offense - MaxKeyValuePairs: 2 - Enabled: false - # Use `sort` instead of `sort_by { |x| x }`. Performance/RedundantSortBy: Enabled: true @@ -1082,18 +741,6 @@ Rails/ReadWriteAttribute: Rails/ScopeArgs: Enabled: true -# Checks the correct usage of time zone aware methods. -# http://danilenko.org/2012/7/6/rails_timezones -Rails/TimeZone: - Enabled: false - -# Use validates :attribute, hash of validations. -Rails/Validation: - Enabled: false - -Rails/UniqBeforePluck: - Enabled: false - ##################### RSpec ################################## # Check that instances are not being stubbed globally. diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 00000000000..9310e711889 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,462 @@ +# This configuration was generated by +# `rubocop --auto-gen-config --exclude-limit 0` +# on 2016-07-13 12:36:08 -0600 using RuboCop version 0.41.2. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 154 +Lint/AmbiguousRegexpLiteral: + Enabled: false + +# Offense count: 43 +# Configuration parameters: AllowSafeAssignment. +Lint/AssignmentInCondition: + Enabled: false + +# Offense count: 14 +Lint/HandleExceptions: + Enabled: false + +# Offense count: 21 +Lint/IneffectiveAccessModifier: + Enabled: false + +# Offense count: 2 +Lint/Loop: + Enabled: false + +# Offense count: 15 +Lint/ShadowingOuterLocalVariable: + Enabled: false + +# Offense count: 3 +# Cop supports --auto-correct. +Lint/StringConversionInInterpolation: + Enabled: false + +# Offense count: 44 +# Cop supports --auto-correct. +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +Lint/UnusedBlockArgument: + Enabled: false + +# Offense count: 129 +# Cop supports --auto-correct. +# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods. +Lint/UnusedMethodArgument: + Enabled: false + +# Offense count: 11 +Lint/UselessAccessModifier: + Enabled: false + +# Offense count: 12 +# Cop supports --auto-correct. +Performance/PushSplat: + Enabled: false + +# Offense count: 2 +# Cop supports --auto-correct. +Performance/RedundantBlockCall: + Enabled: false + +# Offense count: 4 +# Cop supports --auto-correct. +Performance/RedundantMatch: + Enabled: false + +# Offense count: 24 +# Cop supports --auto-correct. +# Configuration parameters: MaxKeyValuePairs. +Performance/RedundantMerge: + Enabled: false + +# Offense count: 60 +Rails/OutputSafety: + Enabled: false + +# Offense count: 128 +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: strict, flexible +Rails/TimeZone: + Enabled: false + +# Offense count: 12 +# Cop supports --auto-correct. +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/Validation: + Enabled: false + +# Offense count: 217 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# SupportedStyles: with_first_parameter, with_fixed_indentation +Style/AlignParameters: + Enabled: false + +# Offense count: 32 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: always, conditionals +Style/AndOr: + Enabled: false + +# Offense count: 47 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: percent_q, bare_percent +Style/BarePercentLiterals: + Enabled: false + +# Offense count: 258 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: braces, no_braces, context_dependent +Style/BracesAroundHashParameters: + Enabled: false + +# Offense count: 5 +Style/CaseEquality: + Enabled: false + +# Offense count: 19 +# Cop supports --auto-correct. +Style/ColonMethodCall: + Enabled: false + +# Offense count: 3 +# Cop supports --auto-correct. +# Configuration parameters: Keywords. +# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW +Style/CommentAnnotation: + Enabled: false + +# Offense count: 34 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly. +# SupportedStyles: assign_to_condition, assign_inside_condition +Style/ConditionalAssignment: + Enabled: false + +# Offense count: 789 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: leading, trailing +Style/DotPosition: + Enabled: false + +# Offense count: 13 +Style/DoubleNegation: + Enabled: false + +# Offense count: 3 +Style/EachWithObject: + Enabled: false + +# Offense count: 30 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: empty, nil, both +Style/EmptyElse: + Enabled: false + +# Offense count: 3 +# Cop supports --auto-correct. +Style/EmptyLiteral: + Enabled: false + +# Offense count: 123 +# Cop supports --auto-correct. +# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. +Style/ExtraSpacing: + Enabled: false + +# Offense count: 7 +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: format, sprintf, percent +Style/FormatString: + Enabled: false + +# Offense count: 48 +# Configuration parameters: MinBodyLength. +Style/GuardClause: + Enabled: false + +# Offense count: 11 +Style/IfInsideElse: + Enabled: false + +# Offense count: 177 +# Cop supports --auto-correct. +# Configuration parameters: MaxLineLength. +Style/IfUnlessModifier: + Enabled: false + +# Offense count: 52 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# SupportedStyles: special_inside_parentheses, consistent, align_brackets +Style/IndentArray: + Enabled: false + +# Offense count: 89 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# SupportedStyles: special_inside_parentheses, consistent, align_braces +Style/IndentHash: + Enabled: false + +# Offense count: 12 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: line_count_dependent, lambda, literal +Style/Lambda: + Enabled: false + +# Offense count: 6 +# Cop supports --auto-correct. +Style/LineEndConcatenation: + Enabled: false + +# Offense count: 13 +# Cop supports --auto-correct. +Style/MethodCallParentheses: + Enabled: false + +# Offense count: 3 +Style/MultilineTernaryOperator: + Enabled: false + +# Offense count: 62 +# Cop supports --auto-correct. +Style/MutableConstant: + Enabled: false + +# Offense count: 10 +# Cop supports --auto-correct. +Style/NestedParenthesizedCalls: + Enabled: false + +# Offense count: 12 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. +# SupportedStyles: skip_modifier_ifs, always +Style/Next: + Enabled: false + +# Offense count: 8 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles. +# SupportedOctalStyles: zero_with_o, zero_only +Style/NumericLiteralPrefix: + Enabled: false + +# Offense count: 29 +# Cop supports --auto-correct. +Style/ParallelAssignment: + Enabled: false + +# Offense count: 208 +# Cop supports --auto-correct. +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Enabled: false + +# Offense count: 11 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: lower_case_q, upper_case_q +Style/PercentQLiterals: + Enabled: false + +# Offense count: 13 +# Cop supports --auto-correct. +Style/PerlBackrefs: + Enabled: false + +# Offense count: 32 +# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. +# NamePrefix: is_, has_, have_ +# NamePrefixBlacklist: is_, has_, have_ +# NameWhitelist: is_a? +Style/PredicateName: + Enabled: false + +# Offense count: 28 +# Cop supports --auto-correct. +Style/PreferredHashMethods: + Enabled: false + +# Offense count: 6 +# Cop supports --auto-correct. +Style/Proc: + Enabled: false + +# Offense count: 20 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: compact, exploded +Style/RaiseArgs: + Enabled: false + +# Offense count: 3 +# Cop supports --auto-correct. +Style/RedundantBegin: + Enabled: false + +# Offense count: 1 +# Cop supports --auto-correct. +Style/RedundantException: + Enabled: false + +# Offense count: 23 +# Cop supports --auto-correct. +Style/RedundantFreeze: + Enabled: false + +# Offense count: 377 +# Cop supports --auto-correct. +Style/RedundantSelf: + Enabled: false + +# Offense count: 94 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. +# SupportedStyles: slashes, percent_r, mixed +Style/RegexpLiteral: + Enabled: false + +# Offense count: 17 +# Cop supports --auto-correct. +Style/RescueModifier: + Enabled: false + +# Offense count: 2 +# Cop supports --auto-correct. +Style/SelfAssignment: + Enabled: false + +# Offense count: 2 +# Configuration parameters: Methods. +# Methods: {"reduce"=>["a", "e"]}, {"inject"=>["a", "e"]} +Style/SingleLineBlockParams: + Enabled: false + +# Offense count: 50 +# Cop supports --auto-correct. +# Configuration parameters: AllowIfMethodIsEmpty. +Style/SingleLineMethods: + Enabled: false + +# Offense count: 14 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: space, no_space +Style/SpaceAroundEqualsInParameterDefault: + Enabled: false + +# Offense count: 119 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: space, no_space +Style/SpaceBeforeBlockBraces: + Enabled: false + +# Offense count: 11 +# Cop supports --auto-correct. +# Configuration parameters: AllowForAlignment. +Style/SpaceBeforeFirstArg: + Enabled: false + +# Offense count: 130 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. +# SupportedStyles: space, no_space +Style/SpaceInsideBlockBraces: + Enabled: false + +# Offense count: 98 +# Cop supports --auto-correct. +Style/SpaceInsideBrackets: + Enabled: false + +# Offense count: 60 +# Cop supports --auto-correct. +Style/SpaceInsideParens: + Enabled: false + +# Offense count: 5 +# Cop supports --auto-correct. +Style/SpaceInsidePercentLiteralDelimiters: + Enabled: false + +# Offense count: 36 +# Cop supports --auto-correct. +# Configuration parameters: SupportedStyles. +# SupportedStyles: use_perl_names, use_english_names +Style/SpecialGlobalVars: + EnforcedStyle: use_perl_names + +# Offense count: 30 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiteralsInInterpolation: + Enabled: false + +# Offense count: 24 +# Cop supports --auto-correct. +# Configuration parameters: IgnoredMethods. +# IgnoredMethods: respond_to, define_method +Style/SymbolProc: + Enabled: false + +# Offense count: 23 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. +# SupportedStyles: comma, consistent_comma, no_comma +Style/TrailingCommaInArguments: + Enabled: false + +# Offense count: 113 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. +# SupportedStyles: comma, consistent_comma, no_comma +Style/TrailingCommaInLiteral: + Enabled: false + +# Offense count: 7 +# Cop supports --auto-correct. +# Configuration parameters: AllowNamedUnderscoreVariables. +Style/TrailingUnderscoreVariable: + Enabled: false + +# Offense count: 90 +# Cop supports --auto-correct. +Style/TrailingWhitespace: + Enabled: false + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist. +# Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym +Style/TrivialAccessors: + Enabled: false + +# Offense count: 3 +# Cop supports --auto-correct. +Style/UnlessElse: + Enabled: false + +# Offense count: 13 +# Cop supports --auto-correct. +Style/UnneededInterpolation: + Enabled: false + +# Offense count: 8 +# Cop supports --auto-correct. +Style/ZeroLengthPredicate: + Enabled: false diff --git a/.teatro.yml b/.teatro.yml deleted file mode 100644 index 30054361981..00000000000 --- a/.teatro.yml +++ /dev/null @@ -1,8 +0,0 @@ -stage: - before: - - cp config/gitlab.teatro.yml config/gitlab.yml - - mkdir /apps/gitlab-satellites - - mkdir /apps/repositories - - database: - - RAILS_ENV=development force=yes bundle exec rake db:create gitlab:setup
\ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG index a977fc3fdbf..fcfc2d7aa30 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,51 +1,84 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.10.0 (unreleased) + - Expose {should,force}_remove_source_branch (Ben Boeckel) + - Disable PostgreSQL statement timeout during migrations + - Fix projects dropdown loading performance with a simplified api cal. !5113 (tiagonbotelho) - Fix commit builds API, return all builds for all pipelines for given commit. !4849 - Replace Haml with Hamlit to make view rendering faster. !3666 + - Refresh the branch cache after `git gc` runs - Refactor repository paths handling to allow multiple git mount points + - Optimize system note visibility checking by memoizing the visible reference count !5070 - Add Application Setting to configure default Repository Path for new projects + - Delete award emoji when deleting a user + - Remove pinTo from Flash and make inline flash messages look nicer !4854 (winniehell) - Wrap code blocks on Activies and Todos page. !4783 (winniehell) - Align flash messages with left side of page content !4959 (winniehell) + - Display tooltip for "Copy to Clipboard" button !5164 (winniehell) + - Use default cursor for table header of project files !5165 (winniehell) + - Store when and yaml variables in builds table - Display last commit of deleted branch in push events !4699 (winniehell) - Escape file extension when parsing search results !5141 (winniehell) - Apply the trusted_proxies config to the rack request object for use with rack_attack + - Upgrade to Rails 4.2.7. !5236 - Add Sidekiq queue duration to transaction metrics. - Add a new column `artifacts_size` to table `ci_builds` !4964 - Let Workhorse serve format-patch diffs + - Display tooltip for mentioned users and groups !5261 (winniehell) + - Added day name to contribution calendar tooltips - Make images fit to the size of the viewport !4810 - Fix check for New Branch button on Issue page !4630 (winniehell) - Fix MR-auto-close text added to description. !4836 + - Support U2F devices in Firefox. !5177 - Fix issue, preventing users w/o push access to sort tags !5105 (redetection) - Add Spring EmojiOne updates. + - Fix fetching LFS objects for private CI projects - Add syntax for multiline blockquote using `>>>` fence !3954 - Fix viewing notification settings when a project is pending deletion + - Updated compare dropdown menus to use GL dropdown + - Eager load award emoji on notes - Fix pagination when sorting by columns with lots of ties (like priority) - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times !5020 - Updated project header design + - Issuable collapsed assignee tooltip is now the users name - Exclude email check from the standard health check - Updated layout for Projects, Groups, Users on Admin area !4424 - Fix changing issue state columns in milestone view + - Update health_check gem to version 2.1.0 - Add notification settings dropdown for groups + - Render inline diffs for multiple changed lines following eachother - Wildcards for protected branches. !4665 - Allow importing from Github using Personal Access Tokens. (Eric K Idema) + - API: Expose `due_date` for issues (Robert Schilling) - API: Todos !3188 (Robert Schilling) - API: Expose shared groups for projects and shared projects for groups !5050 (Robert Schilling) - Add "Enabled Git access protocols" to Application Settings + - Diffs will create button/diff form on demand no on server side + - Reduce size of HTML used by diff comment forms - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt) - Only show New Snippet button to users that can create snippets. - PipelinesFinder uses git cache data + - Track a user who created a pipeline + - Actually render old and new sections of parallel diff next to each other - Throttle the update of `project.pushes_since_gc` to 1 minute. + - Allow expanding and collapsing files in diff view (!4990) + - Collapse large diffs by default (!4990) + - Fix mentioned users list on diff notes + - Fix creation of deployment on build that is retried, redeployed or rollback - Check for conflicts with existing Project's wiki path when creating a new project. - Show last push widget in upstream after push to fork + - Fix stage status shown for pipelines + - Cache todos pending/done dashboard query counts. - Don't instantiate a git tree on Projects show default view - Bump Rinku to 2.0.0 - Remove unused front-end variable -> default_issues_tracker + - ObjectRenderer retrieve renderer content using Rails.cache.read_multi - Better caching of git calls on ProjectsController#show. - Avoid to retrieve MR closes_issues as much as possible. - Add API endpoint for a group issues !4520 (mahcsig) - Add Bugzilla integration !4930 (iamtjg) - Instrument Rinku usage + - Be explicit to define merge request discussion variables - Metrics for Rouge::Plugins::Redcarpet and Rouge::Formatters::HTMLGitlab - RailsCache metris now includes fetch_hit/fetch_miss and read_hit/read_miss info. - Allow [ci skip] to be in any case and allow [skip ci]. !4785 (simon_w) @@ -55,14 +88,47 @@ v 8.10.0 (unreleased) - Add basic system information like memory and disk usage to the admin panel - Don't garbage collect commits that have related DB records like comments - More descriptive message for git hooks and file locks + - Aliases of award emoji should be stored as original name. !5060 (dixpac) - Handle custom Git hook result in GitLab UI + - Allow to access Container Registry for Public and Internal projects - Allow '?', or '&' for label names + - Support redirected blobs for Container Registry integration - Fix importer for GitHub Pull Requests when a branch was reused across Pull Requests - Add date when user joined the team on the member page - Fix 404 redirect after validation fails importing a GitLab project - Added setting to set new users by default as external !4545 (Dravere) - Add min value for project limit field on user's form !3622 (jastkand) + - Reset project pushes_since_gc when we enqueue the git gc call - Add reminder to not paste private SSH keys !4399 (Ingo Blechschmidt) + - Remove duplicate `description` field in `MergeRequest` entities (Ben Boeckel) + - Style of import project buttons were fixed in the new project page. !5183 (rdemirbay) + - Fix GitHub client requests when rate limit is disabled + - Optimistic locking for Issues and Merge Requests (Title and description overriding prevention) + - Redesign Builds and Pipelines pages + - Change status color and icon for running builds + - Fix markdown rendering for: consecutive labels references, label references that begin with a digit or contains `.` + - Project export filename now includes the project and namespace path + - Fix last update timestamp on issues not preserved on gitlab.com and project imports + - Fix issues importing projects from EE to CE + - Fix creating group with space in group path + - Create Todos for Issue author when assign or mention himself (Katarzyna Kobierska) + - Limit the number of retries on error to 3 for exporting projects + - Allow empty repositories on project import/export + +v 8.9.6 + - Fix importing of events under notes for GitLab projects. !5154 + - Fix log statements in import/export. !5129 + - Fix commit avatar alignment in compare view. !5128 + - Fix broken migration in MySQL. !5005 + - Overwrite Host and X-Forwarded-Host headers in NGINX !5213 + - Keeps issue number when importing from Gitlab.com + - Add Pending tab for Builds (Katarzyna Kobierska, Urszula Budziszewska) + +v 8.9.7 (unreleased) + - Fix import_data wrongly saved as a result of an invalid import_url + +v 8.9.6 + - Fix importing of events under notes for GitLab projects v 8.9.5 - Add more debug info to import/export and memory killer. !5108 @@ -223,6 +289,7 @@ v 8.9.0 - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database - Changed the Slack build message to use the singular duration if necessary (Aran Koning) - Fix race condition on merge when build succeeds + - Added shortcut to focus filter search fields and added documentation #18120 - Links from a wiki page to other wiki pages should be rewritten as expected - Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos) - Added navigation shortcuts to the project pipelines, milestones, builds and forks page. !4393 @@ -2184,8 +2251,6 @@ v 7.7.0 - 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 @@ -2611,13 +2676,13 @@ v 6.5.0 - Files API supports base64 encoded content (sponsored by O'Reilly Media) - Added support for Go's repository retrieval (Bruno Albuquerque) -v6.4.3 +v 6.4.3 - Don't use unicorn worker killer if PhusionPassenger is defined -v6.4.2 +v 6.4.2 - Fixed wrong behaviour of script/upgrade.rb -v6.4.1 +v 6.4.1 - Fixed bug with repository rename - Fixed bug with project transfer diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4472214778..14ff05c9aa3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -145,7 +145,8 @@ might be edited to make them small and simple. You are encouraged to use the template below for feature proposals. ``` -## Description including problem, use cases, benefits, and/or goals +## Description +Include problem, use cases, benefits, and/or goals ## Proposal diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 879be8a98fc..e7c7d3cc3c8 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.7.7 +0.7.8 @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'rails', '4.2.6' +gem 'rails', '4.2.7' gem 'rails-deprecated_sanitizer', '~> 1.0.3' # Responders respond_to and respond_with @@ -61,7 +61,7 @@ gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap' # Git Wiki # Required manually in config/initializers/gollum.rb to control load order -gem 'gollum-lib', '~> 4.1.0', require: false +gem 'gollum-lib', '~> 4.2', require: false gem 'gollum-rugged_adapter', '~> 0.4.2', require: false # Language detection @@ -105,7 +105,7 @@ gem 'seed-fu', '~> 2.3.5' # Markdown and HTML processing gem 'html-pipeline', '~> 1.11.0' gem 'task_list', '~> 1.0.2', require: 'task_list/railtie' -gem 'github-markup', '~> 1.3.1' +gem 'github-markup', '~> 1.4' gem 'redcarpet', '~> 3.3.3' gem 'RedCloth', '~> 4.3.2' gem 'rdoc', '~>3.6' @@ -113,7 +113,7 @@ gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.2' -gem 'rouge', '~> 1.11' +gem 'rouge', '~> 2.0' # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM @@ -299,7 +299,7 @@ group :development, :test do gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-teaspoon', '~> 0.0.2' - gem 'rubocop', '~> 0.40.0', require: false + gem 'rubocop', '~> 0.41.2', require: false gem 'rubocop-rspec', '~> 1.5.0', require: false gem 'scss_lint', '~> 0.47.0', require: false gem 'simplecov', '~> 0.11.0', require: false @@ -344,7 +344,7 @@ gem 'oauth2', '~> 1.2.0' gem 'paranoia', '~> 2.0' # Health check -gem 'health_check', '~> 1.5.1' +gem 'health_check', '~> 2.1.0' # System information gem 'vmstat', '~> 2.1.0' diff --git a/Gemfile.lock b/Gemfile.lock index 721ab9ddc5d..0987fd5665a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,34 +3,34 @@ GEM specs: RedCloth (4.3.2) ace-rails-ap (4.0.2) - actionmailer (4.2.6) - actionpack (= 4.2.6) - actionview (= 4.2.6) - activejob (= 4.2.6) + actionmailer (4.2.7) + actionpack (= 4.2.7) + actionview (= 4.2.7) + activejob (= 4.2.7) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.6) - actionview (= 4.2.6) - activesupport (= 4.2.6) + actionpack (4.2.7) + actionview (= 4.2.7) + activesupport (= 4.2.7) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.6) - activesupport (= 4.2.6) + actionview (4.2.7) + activesupport (= 4.2.7) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.6) - activesupport (= 4.2.6) + activejob (4.2.7) + activesupport (= 4.2.7) globalid (>= 0.3.0) - activemodel (4.2.6) - activesupport (= 4.2.6) + activemodel (4.2.7) + activesupport (= 4.2.7) builder (~> 3.1) - activerecord (4.2.6) - activemodel (= 4.2.6) - activesupport (= 4.2.6) + activerecord (4.2.7) + activemodel (= 4.2.7) + activesupport (= 4.2.7) arel (~> 6.0) activerecord-session_store (1.0.0) actionpack (>= 4.0, < 5.1) @@ -38,7 +38,7 @@ GEM multi_json (~> 1.11, >= 1.11.2) rack (>= 1.5.2, < 3) railties (>= 4.0, < 5.1) - activesupport (4.2.6) + activesupport (4.2.7) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -58,7 +58,7 @@ GEM faraday_middleware-multi_json (~> 0.0) oauth2 (~> 1.0) asciidoctor (1.5.3) - ast (2.2.0) + ast (2.3.0) attr_encrypted (3.0.1) encryptor (~> 3.0.0) attr_required (1.0.0) @@ -264,7 +264,7 @@ GEM escape_utils (~> 1.1.0) mime-types (>= 1.19) rugged (>= 0.23.0b) - github-markup (1.3.3) + github-markup (1.4.0) gitlab-flowdock-git-hook (1.0.1) flowdock (~> 0.7) gitlab-grit (>= 2.4.1) @@ -287,13 +287,13 @@ GEM rubyntlm (~> 0.3) globalid (0.3.6) activesupport (>= 4.1.0) - gollum-grit_adapter (1.0.0) + gollum-grit_adapter (1.0.1) gitlab-grit (~> 2.7, >= 2.7.1) - gollum-lib (4.1.0) - github-markup (~> 1.3.3) + gollum-lib (4.2.1) + github-markup (~> 1.4.0) gollum-grit_adapter (~> 1.0) nokogiri (~> 1.6.4) - rouge (~> 1.9) + rouge (~> 2.0) sanitize (~> 2.1.0) stringex (~> 2.5.1) gollum-rugged_adapter (0.4.2) @@ -322,8 +322,8 @@ GEM thor tilt hashie (3.4.3) - health_check (1.5.1) - rails (>= 2.3.0) + health_check (2.1.0) + rails (>= 4.0) hipchat (1.5.2) httparty mimemagic @@ -473,7 +473,7 @@ GEM orm_adapter (0.5.0) paranoia (2.1.4) activerecord (~> 4.0) - parser (2.3.1.0) + parser (2.3.1.2) ast (~> 2.2) pg (0.18.4) pkg-config (1.1.7) @@ -515,16 +515,16 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.6) - actionmailer (= 4.2.6) - actionpack (= 4.2.6) - actionview (= 4.2.6) - activejob (= 4.2.6) - activemodel (= 4.2.6) - activerecord (= 4.2.6) - activesupport (= 4.2.6) + rails (4.2.7) + actionmailer (= 4.2.7) + actionpack (= 4.2.7) + actionview (= 4.2.7) + activejob (= 4.2.7) + activemodel (= 4.2.7) + activerecord (= 4.2.7) + activesupport (= 4.2.7) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.6) + railties (= 4.2.7) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -534,9 +534,9 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (4.2.6) - actionpack (= 4.2.6) - activesupport (= 4.2.6) + railties (4.2.7) + actionpack (= 4.2.7) + activesupport (= 4.2.7) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.1.0) @@ -578,7 +578,7 @@ GEM railties (>= 4.2.0, < 5.1) rinku (2.0.0) rotp (2.1.2) - rouge (1.11.0) + rouge (2.0.3) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -606,8 +606,8 @@ GEM rspec-retry (0.4.5) rspec-core rspec-support (3.5.0) - rubocop (0.40.0) - parser (>= 2.3.1.0, < 3.0) + rubocop (0.41.2) + parser (>= 2.3.1.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) @@ -697,7 +697,7 @@ GEM spring (>= 0.9.1) spring-commands-teaspoon (0.0.2) spring (>= 0.9.1) - sprockets (3.6.2) + sprockets (3.6.3) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.1.1) @@ -758,7 +758,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.2) - unicode-display_width (1.0.5) + unicode-display_width (1.1.0) unicorn (4.9.0) kgio (~> 2.6) rack @@ -859,18 +859,18 @@ DEPENDENCIES gemnasium-gitlab-service (~> 0.2) gemojione (~> 2.6) github-linguist (~> 4.7.0) - github-markup (~> 1.3.1) + github-markup (~> 1.4) gitlab-flowdock-git-hook (~> 1.0.1) gitlab_git (~> 10.2) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) - gollum-lib (~> 4.1.0) + gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) gon (~> 6.0.1) grape (~> 0.13.0) grape-entity (~> 0.4.2) hamlit (~> 2.5) - health_check (~> 1.5.1) + health_check (~> 2.1.0) hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) httparty (~> 0.13.3) @@ -920,7 +920,7 @@ DEPENDENCIES rack-attack (~> 4.3.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) - rails (= 4.2.6) + rails (= 4.2.7) rails-deprecated_sanitizer (~> 1.0.3) rainbow (~> 2.1.0) rblineprof (~> 0.3.6) @@ -933,11 +933,11 @@ DEPENDENCIES request_store (~> 1.3.0) rerun (~> 0.11.0) responders (~> 2.0) - rouge (~> 1.11) + rouge (~> 2.0) rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.5.0) rspec-retry (~> 0.4.5) - rubocop (~> 0.40.0) + rubocop (~> 0.41.2) rubocop-rspec (~> 1.5.0) ruby-fogbugz (~> 0.2.1) sanitize (~> 2.0) diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index cf46f15a156..89b0ac697ed 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -3,7 +3,7 @@ groupPath: "/api/:version/groups/:id.json" namespacesPath: "/api/:version/namespaces.json" groupProjectsPath: "/api/:version/groups/:id/projects.json" - projectsPath: "/api/:version/projects.json" + projectsPath: "/api/:version/projects.json?simple=true" labelsPath: "/api/:version/projects/:id/labels" licensePath: "/api/:version/licenses/:key" gitignorePath: "/api/:version/gitignores/:key" diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 64da503c35f..eceff6d91d5 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -47,14 +47,12 @@ #= require date.format #= require_directory ./behaviors #= require_directory ./blob -#= require_directory ./ci #= require_directory ./commit #= require_directory ./extensions #= require_directory ./lib/utils #= require_directory ./u2f #= require_directory . #= require fuzzaldrin-plus -#= require u2f window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/build.coffee index 74691b2c1b5..cf203ea43a0 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/build.coffee @@ -1,9 +1,9 @@ -class @CiBuild +class @Build @interval: null @state: null constructor: (@page_url, @build_url, @build_status, @state) -> - clearInterval(CiBuild.interval) + clearInterval(Build.interval) # Init breakpoint checker @bp = Breakpoints.get() @@ -40,7 +40,7 @@ class @CiBuild # Check for new build output if user still watching build page # Only valid for runnig build when output changes during time # - CiBuild.interval = setInterval => + Build.interval = setInterval => if window.location.href.split("#").first() is @page_url @getBuildTrace() , 4000 diff --git a/app/assets/javascripts/ci/application.js.coffee b/app/assets/javascripts/ci/application.js.coffee deleted file mode 100644 index ca24c1d759f..00000000000 --- a/app/assets/javascripts/ci/application.js.coffee +++ /dev/null @@ -1,12 +0,0 @@ -#= require pager -#= require jquery_nested_form -#= require_tree . - -$(document).on 'click', '.assign-all-runner', -> - $(this).replaceWith('<i class="fa fa-refresh fa-spin"></i> Assign in progress..') - -window.unbindEvents = -> - $(document).unbind('scroll') - $(document).off('scroll') - -document.addEventListener("page:fetch", unbindEvents) diff --git a/app/assets/javascripts/ci/projects.js.coffee b/app/assets/javascripts/ci/projects.js.coffee deleted file mode 100644 index e6406011d11..00000000000 --- a/app/assets/javascripts/ci/projects.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -$(document).on 'click', '.badge-codes-toggle', -> - $('.badge-codes-block').toggleClass("hide") - return false diff --git a/app/assets/javascripts/compare_autocomplete.js.coffee b/app/assets/javascripts/compare_autocomplete.js.coffee new file mode 100644 index 00000000000..7ad9fd97637 --- /dev/null +++ b/app/assets/javascripts/compare_autocomplete.js.coffee @@ -0,0 +1,41 @@ +class @CompareAutocomplete + constructor: -> + @initDropdown() + + initDropdown: -> + $('.js-compare-dropdown').each -> + $dropdown = $(@) + selected = $dropdown.data('selected') + + $dropdown.glDropdown( + data: (term, callback) -> + $.ajax( + url: $dropdown.data('refs-url') + data: + ref: $dropdown.data('ref') + ).done (refs) -> + callback(refs) + selectable: true + filterable: true + filterByText: true + fieldName: $dropdown.attr('name') + filterInput: 'input[type="text"]' + renderRow: (ref) -> + if ref.header? + $('<li />') + .addClass('dropdown-header') + .text(ref.header) + else + link = $('<a />') + .attr('href', '#') + .addClass(if ref is selected then 'is-active' else '') + .text(ref) + .attr('data-ref', escape(ref)) + + $('<li />') + .append(link) + id: (obj, $el) -> + $el.attr('data-ref') + toggleLabel: (obj, $el) -> + $el.text().trim() + ) diff --git a/app/assets/javascripts/diff.js.coffee b/app/assets/javascripts/diff.js.coffee index 6d9b364cb8d..c132cc8c542 100644 --- a/app/assets/javascripts/diff.js.coffee +++ b/app/assets/javascripts/diff.js.coffee @@ -1,6 +1,9 @@ class @Diff UNFOLD_COUNT = 20 constructor: -> + $('.files .diff-file').singleFileDiff() + @filesCommentButton = $('.files .diff-file').filesCommentButton() + $(document).off('click', '.js-unfold') $(document).on('click', '.js-unfold', (event) => target = $(event.target) @@ -36,7 +39,7 @@ class @Diff # see https://gitlab.com/gitlab-org/gitlab-ce/issues/707 indent: 1 - $.get(link, params, (response) => + $.get(link, params, (response) -> target.parent().replaceWith(response) ) ) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 74fd77cf7ab..afaa6407b05 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -137,6 +137,8 @@ class Dispatcher new Project() new ProjectAvatar() switch path[1] + when 'compare' + new CompareAutocomplete() when 'edit' shortcut_handler = new ShortcutsNavigation() new ProjectNew() diff --git a/app/assets/javascripts/files_comment_button.js.coffee b/app/assets/javascripts/files_comment_button.js.coffee new file mode 100644 index 00000000000..db0bf7082a9 --- /dev/null +++ b/app/assets/javascripts/files_comment_button.js.coffee @@ -0,0 +1,97 @@ +class @FilesCommentButton + COMMENT_BUTTON_CLASS = '.add-diff-note' + COMMENT_BUTTON_TEMPLATE = _.template '<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>' + LINE_HOLDER_CLASS = '.line_holder' + LINE_NUMBER_CLASS = 'diff-line-num' + LINE_CONTENT_CLASS = 'line_content' + UNFOLDABLE_LINE_CLASS = 'js-unfold' + EMPTY_CELL_CLASS = 'empty-cell' + OLD_LINE_CLASS = 'old_line' + NEW_CLASS = 'new' + LINE_COLUMN_CLASSES = ".#{LINE_NUMBER_CLASS}, .line_content" + TEXT_FILE_SELECTOR = '.text-file' + DEBOUNCE_TIMEOUT_DURATION = 100 + + constructor: (@filesContainerElement) -> + @VIEW_TYPE = $('input#view[type=hidden]').val() + + debounce = _.debounce @render, DEBOUNCE_TIMEOUT_DURATION + + $(document) + .on 'mouseover', LINE_COLUMN_CLASSES, debounce + .on 'mouseleave', LINE_COLUMN_CLASSES, @destroy + + render: (e) => + $currentTarget = $(e.currentTarget) + buttonParentElement = @getButtonParent $currentTarget + return unless @shouldRender e, buttonParentElement + + textFileElement = @getTextFileElement $currentTarget + lineContentElement = @getLineContent $currentTarget + + buttonParentElement.append @buildButton + noteableType: textFileElement.attr 'data-noteable-type' + noteableID: textFileElement.attr 'data-noteable-id' + commitID: textFileElement.attr 'data-commit-id' + noteType: lineContentElement.attr 'data-note-type' + position: lineContentElement.attr 'data-position' + lineType: lineContentElement.attr 'data-line-type' + discussionID: lineContentElement.attr 'data-discussion-id' + lineCode: lineContentElement.attr 'data-line-code' + return + + destroy: (e) => + return if @isMovingToSameType e + $(COMMENT_BUTTON_CLASS, @getButtonParent $(e.currentTarget)).remove() + return + + buildButton: (buttonAttributes) -> + initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE + COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr 1 + $(initializedButtonTemplate).attr + 'data-noteable-type': buttonAttributes.noteableType + 'data-noteable-id': buttonAttributes.noteableID + 'data-commit-id': buttonAttributes.commitID + 'data-note-type': buttonAttributes.noteType + 'data-line-code': buttonAttributes.lineCode + 'data-position': buttonAttributes.position + 'data-discussion-id': buttonAttributes.discussionID + 'data-line-type': buttonAttributes.lineType + + getTextFileElement: (hoveredElement) -> + $(hoveredElement.closest TEXT_FILE_SELECTOR) + + getLineContent: (hoveredElement) -> + return hoveredElement if hoveredElement.hasClass LINE_CONTENT_CLASS + + $(".#{LINE_CONTENT_CLASS + @diffTypeClass hoveredElement}", hoveredElement.parent()) + + getButtonParent: (hoveredElement) -> + if @VIEW_TYPE is 'inline' + return hoveredElement if hoveredElement.hasClass OLD_LINE_CLASS + + $(".#{OLD_LINE_CLASS}", hoveredElement.parent()) + else + return hoveredElement if hoveredElement.hasClass LINE_NUMBER_CLASS + + $(".#{LINE_NUMBER_CLASS + @diffTypeClass hoveredElement}", hoveredElement.parent()) + + diffTypeClass: (hoveredElement) -> + if hoveredElement.hasClass(NEW_CLASS) then '.new' else '.old' + + isMovingToSameType: (e) -> + newButtonParent = @getButtonParent $(e.toElement) + return false unless newButtonParent + newButtonParent.is @getButtonParent $(e.currentTarget) + + shouldRender: (e, buttonParentElement) -> + (not buttonParentElement.hasClass(EMPTY_CELL_CLASS) and \ + not buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) and \ + $(COMMENT_BUTTON_CLASS, buttonParentElement).length is 0) + +$.fn.filesCommentButton = -> + return unless this and @parent().data('can-create-note')? + + @each -> + unless $.data this, 'filesCommentButton' + $.data this, 'filesCommentButton', new FilesCommentButton $(this) diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee index b76d214790a..5a493041538 100644 --- a/app/assets/javascripts/flash.js.coffee +++ b/app/assets/javascripts/flash.js.coffee @@ -1,24 +1,28 @@ class @Flash - constructor: (message, type = 'alert')-> - @flash = $(".flash-container") - @flash.html("") + hideFlash = -> $(@).fadeOut() - innerDiv = $('<div/>', + constructor: (message, type = 'alert', parent = null)-> + if parent + @flashContainer = parent.find('.flash-container') + else + @flashContainer = $('.flash-container-page') + + @flashContainer.html('') + + flash = $('<div/>', class: "flash-#{type}" ) - innerDiv.appendTo(".flash-container") + flash.on 'click', hideFlash - textDiv = $("<div/>", - class: "flash-text", + textDiv = $('<div/>', + class: 'flash-text', text: message ) - textDiv.appendTo(innerDiv) + textDiv.appendTo(flash) - if @flash.parent().hasClass('content-wrapper') + if @flashContainer.parent().hasClass('content-wrapper') textDiv.addClass('container-fluid container-limited') - @flash.click -> $(@).fadeOut() - @flash.show() + flash.appendTo(@flashContainer) + @flashContainer.show() - pinTo: (selector) -> - @flash.detach().appendTo(selector) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 1c65e833d47..1b0d0db8954 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -456,6 +456,8 @@ class GitLabDropdown rowClicked: (el) -> fieldName = @options.fieldName + isInput = $(@el).is('input') + if @renderedData groupName = el.data('group') if groupName @@ -466,10 +468,19 @@ class GitLabDropdown selectedObject = @renderedData[selectedIndex] value = if @options.id then @options.id(selectedObject, el) else selectedObject.id - field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") + + if isInput + field = $(@el) + else + field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") + if el.hasClass(ACTIVE_CLASS) el.removeClass(ACTIVE_CLASS) - field.remove() + + if isInput + field.val('') + else + field.remove() # Toggle the dropdown label if @options.toggleLabel @@ -490,7 +501,9 @@ class GitLabDropdown else if not @options.multiSelect or el.hasClass('dropdown-clear-active') @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS - @dropdown.parent().find("input[name='#{fieldName}']").remove() + + unless isInput + @dropdown.parent().find("input[name='#{fieldName}']").remove() if !value? field.remove() @@ -505,7 +518,9 @@ class GitLabDropdown if !field.length and fieldName @addInput(fieldName, value) else - field.val value + field + .val value + .trigger 'change' return selectedObject diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee index c71d4ecf505..7f795f8096b 100644 --- a/app/assets/javascripts/issuable.js.coffee +++ b/app/assets/javascripts/issuable.js.coffee @@ -32,13 +32,11 @@ issuable_created = false $search = $('#issue_search') $form = $('.js-filter-form') $input = $("input[name='#{$search.attr('name')}']", $form) - if $input.length is 0 $form.append "<input type='hidden' name='#{$search.attr('name')}' value='#{_.escape($search.val())}'/>" else $input.val $search.val() - - Issuable.filterResults $form + Issuable.filterResults $form if $search.val() isnt '' , 500) initLabelFilterRemove: -> diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.coffee b/app/assets/javascripts/lib/utils/datetime_utility.js.coffee index 948d6dbf07e..178963fe0aa 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js.coffee +++ b/app/assets/javascripts/lib/utils/datetime_utility.js.coffee @@ -2,10 +2,14 @@ w.gl ?= {} w.gl.utils ?= {} + w.gl.utils.days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] w.gl.utils.formatDate = (datetime) -> dateFormat(datetime, 'mmm d, yyyy h:MMtt Z') + w.gl.utils.getDayName = (date) -> + this.days[date.getDay()] + w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) -> $timeagoEls.each( -> $el = $(@) diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 894f80586f1..86539e0d725 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -153,17 +153,18 @@ class @MergeRequestTabs loadDiff: (source) -> return if @diffsLoaded - @_get url: "#{source}.json" + @_location.search success: (data) => $('#diffs').html data.html gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) $('#diffs .js-syntax-highlight').syntaxHighlight() + $('#diffs .diff-file').singleFileDiff() @expandViewContainer() if @diffViewType() is 'parallel' @diffsLoaded = true @scrollToElement("#diffs") @highlighSelectedLine() + @filesCommentButton = $('.files .diff-file').filesCommentButton() $(document) .off 'click', '.diff-line-num a' diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 0b7d8f64456..0ea54faae1a 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -194,8 +194,7 @@ class @Notes renderNote: (note) -> unless note.valid if note.award - flash = new Flash('You have already awarded this emoji!', 'alert') - flash.pinTo('.header-content') + new Flash('You have already awarded this emoji!', 'alert') return if note.award @@ -325,6 +324,8 @@ class @Notes form.find("#note_position").remove() form.find("#note_type").remove() + @parentTimeline = form.parents('.timeline') + ### General note form setup. @@ -357,8 +358,7 @@ class @Notes @renderNote(note) addNoteError: (xhr, note, status) => - flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert') - flash.pinTo('.md-area') + new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', @parentTimeline) ### Called in response to the new note form being submitted diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee index e4c4bf3b273..a7d78d9e461 100644 --- a/app/assets/javascripts/projects_list.js.coffee +++ b/app/assets/javascripts/projects_list.js.coffee @@ -5,13 +5,12 @@ this.initPagination() initSearch: -> - @timer = null - $(".projects-list-filter").on('keyup', -> - clearTimeout(@timer) - @timer = setTimeout(ProjectsList.filterResults, 500) - ) + projectsListFilter = $('.projects-list-filter') + debounceFilter = _.debounce ProjectsList.filterResults, 500 + projectsListFilter.on 'keyup', (e) -> + debounceFilter() if projectsListFilter.val() isnt '' - filterResults: => + filterResults: -> $('.projects-list-holder').fadeTo(250, 0.5) form = null diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee index 3319a67a79d..8c8689bacee 100644 --- a/app/assets/javascripts/shortcuts.js.coffee +++ b/app/assets/javascripts/shortcuts.js.coffee @@ -2,9 +2,10 @@ class @Shortcuts constructor: (skipResetBindings) -> @enabledHelp = [] Mousetrap.reset() if not skipResetBindings - Mousetrap.bind('?', @onToggleHelp) - Mousetrap.bind('s', Shortcuts.focusSearch) - Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview) + Mousetrap.bind '?', @onToggleHelp + Mousetrap.bind 's', Shortcuts.focusSearch + Mousetrap.bind 'f', (e) => @focusFilter e + Mousetrap.bind ['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL? onToggleHelp: (e) => @@ -32,10 +33,16 @@ class @Shortcuts $('.js-more-help-button').remove() ) + focusFilter: (e) -> + @filterInput ?= $('input[type=search]', '.nav-controls') + @filterInput.focus() + e.preventDefault() + @focusSearch: (e) -> $('#search').focus() e.preventDefault() + $(document).on 'click.more_help', '.js-more-help-button', (e) -> $(@).remove() $('.hidden-shortcut').show() diff --git a/app/assets/javascripts/single_file_diff.js.coffee b/app/assets/javascripts/single_file_diff.js.coffee new file mode 100644 index 00000000000..f3e225c3728 --- /dev/null +++ b/app/assets/javascripts/single_file_diff.js.coffee @@ -0,0 +1,54 @@ +class @SingleFileDiff + + WRAPPER = '<div class="diff-content diff-wrap-lines"></div>' + LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>' + ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>' + COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>' + + constructor: (@file) -> + @content = $('.diff-content', @file) + @diffForPath = @content.find('[data-diff-for-path]').data 'diff-for-path' + @isOpen = !@diffForPath + + if @diffForPath + @collapsedContent = @content + @loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide() + @content = null + @collapsedContent.after(@loadingContent) + else + @collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide() + @content.after(@collapsedContent) + + @collapsedContent.on 'click', @toggleDiff + + $('.file-title > a', @file).on 'click', @toggleDiff + + toggleDiff: (e) => + @isOpen = !@isOpen + if not @isOpen and not @hasError + @content.hide() + @collapsedContent.show() + else if @content + @collapsedContent.hide() + @content.show() + else + @getContentHTML() + + getContentHTML: -> + @collapsedContent.hide() + @loadingContent.show() + $.get @diffForPath, (data) => + @loadingContent.hide() + if data.html + @content = $(data.html) + @content.syntaxHighlight() + else + @hasError = true + @content = $(ERROR_HTML) + @collapsedContent.after(@content) + return + +$.fn.singleFileDiff = -> + return @each -> + if not $.data this, 'singleFileDiff' + $.data this, 'singleFileDiff', new SingleFileDiff this diff --git a/app/assets/javascripts/u2f/authenticate.js.coffee b/app/assets/javascripts/u2f/authenticate.js.coffee index 6deb902c8de..918c0a560fd 100644 --- a/app/assets/javascripts/u2f/authenticate.js.coffee +++ b/app/assets/javascripts/u2f/authenticate.js.coffee @@ -6,8 +6,20 @@ class @U2FAuthenticate constructor: (@container, u2fParams) -> @appId = u2fParams.app_id - @challenges = u2fParams.challenges - @signRequests = u2fParams.sign_requests + @challenge = u2fParams.challenge + + # The U2F Javascript API v1.1 requires a single challenge, with + # _no challenges per-request_. The U2F Javascript API v1.0 requires a + # challenge per-request, which is done by copying the single challenge + # into every request. + # + # In either case, we don't need the per-request challenges that the server + # has generated, so we can remove them. + # + # Note: The server library fixes this behaviour in (unreleased) version 1.0.0. + # This can be removed once we upgrade. + # https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4 + @signRequests = u2fParams.sign_requests.map (request) -> _(request).omit('challenge') start: () => if U2FUtil.isU2FSupported() @@ -16,7 +28,7 @@ class @U2FAuthenticate @renderNotSupported() authenticate: () => - u2f.sign(@appId, @challenges, @signRequests, (response) => + u2f.sign(@appId, @challenge, @signRequests, (response) => if response.errorCode error = new U2FError(response.errorCode) @renderError(error); diff --git a/app/assets/javascripts/u2f/util.js.coffee b/app/assets/javascripts/u2f/util.js.coffee new file mode 100644 index 00000000000..5ef324f609d --- /dev/null +++ b/app/assets/javascripts/u2f/util.js.coffee @@ -0,0 +1,3 @@ +class @U2FUtil + @isU2FSupported: -> + window.u2f diff --git a/app/assets/javascripts/u2f/util.js.coffee.erb b/app/assets/javascripts/u2f/util.js.coffee.erb deleted file mode 100644 index d59341c38b9..00000000000 --- a/app/assets/javascripts/u2f/util.js.coffee.erb +++ /dev/null @@ -1,15 +0,0 @@ -# Helper class for U2F (universal 2nd factor) device registration and authentication. - -class @U2FUtil - @isU2FSupported: -> - if @testMode - true - else - gon.u2f.browser_supports_u2f - - @enableTestMode: -> - @testMode = true - -<% if Rails.env.test? %> -U2FUtil.enableTestMode(); -<% end %> diff --git a/app/assets/javascripts/users/calendar.js.coffee b/app/assets/javascripts/users/calendar.js.coffee index c081f023b04..c49ba5186f2 100644 --- a/app/assets/javascripts/users/calendar.js.coffee +++ b/app/assets/javascripts/users/calendar.js.coffee @@ -87,14 +87,15 @@ class @Calendar .attr 'width', @daySize .attr 'height', @daySize .attr 'title', (stamp) => + date = new Date(stamp.date) contribText = 'No contributions' if stamp.count > 0 contribText = "#{stamp.count} contribution#{if stamp.count > 1 then 's' else ''}" - date = dateFormat(stamp.date, 'mmm d, yyyy') + dateText = dateFormat(date, 'mmm d, yyyy') - "#{contribText}<br />#{date}" + "#{contribText}<br />#{gl.utils.getDayName(date)} #{dateText}" .attr 'class', 'user-contrib-cell js-tooltip' .attr 'fill', (stamp) => if stamp.count isnt 0 diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 4e032ab1ff1..344be811e0d 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -56,6 +56,11 @@ class @UsersSelect username: '' avatar: '' $value.html(assigneeTemplate(user)) + + $collapsedSidebar + .attr('title', user.name) + .tooltip('fixTitle') + $collapsedSidebar.html(collapsedAssigneeTemplate(user)) @@ -63,7 +68,6 @@ class @UsersSelect '<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> - <span class="author">Toni Boehm</span> </a> <% } else { %> <i class="fa fa-user"></i> diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index bb8d71fbae8..8b6ddf8ba18 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -20,6 +20,7 @@ } &.s16 { width: 16px; height: 16px; margin-right: 6px; } + &.s20 { width: 20px; height: 20px; margin-right: 7px; } &.s24 { width: 24px; height: 24px; margin-right: 8px; } &.s26 { width: 26px; height: 26px; margin-right: 8px; } &.s32 { width: 32px; height: 32px; margin-right: 10px; } diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index d28cda6d62d..540718197e0 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -20,9 +20,12 @@ .blank-state-icon { padding-bottom: 20px; + color: $gray-darkest; + font-size: 56px; - path { - fill: $gray-darkest; + path, + polygon { + fill: currentColor; } } @@ -37,6 +40,10 @@ margin-top: 0; margin-bottom: $gl-padding; font-size: 15px; + + > strong { + font-weight: 600; + } } .blank-state-welcome-title { diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 41e77a4ac68..ad94e457cfd 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -16,6 +16,15 @@ font-weight: normal; font-size: 16px; line-height: 36px; + + &.diff-collapsed { + padding: 5px; + cursor: pointer; + + &:hover { + background-color: $row-hover; + } + } } .row-content-block { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index f8aecd0558d..c1e5305644b 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -270,21 +270,6 @@ table { } } -.dashboard-intro-icon { - float: left; - text-align: center; - font-size: 32px; - color: #aaa; - width: 60px; -} - -.dashboard-intro-text { - display: inline-block; - margin-left: -60px; - padding-left: 60px; - width: 100%; -} - .btn-sign-in { text-shadow: none; diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index a951a2b97fe..0c21d0240b3 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -1,8 +1,8 @@ .flash-container { cursor: pointer; margin: 0; + margin-bottom: $gl-padding; font-size: 14px; - width: 100%; z-index: 100; .flash-notice { @@ -18,9 +18,27 @@ } .flash-notice, .flash-alert { - .container-fluid.flash-text { + border-radius: $border-radius-default; + + .container-fluid.container-limited.flash-text { background: transparent; } } + + &.flash-container-page { + margin-bottom: 0; + + .flash-notice, .flash-alert { + border-radius: 0; + } + } +} + +@media (max-width: $screen-md-min) { + ul.notes { + .flash-container.timeline-content { + margin-left: 0; + } + } } diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index d1ff63bd099..3673b81f183 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -11,7 +11,6 @@ .toggle-nav-collapse, .pin-nav-btn { color: $color-light; - background: $color; &:hover { color: $white-light; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 188823054fd..d52e8f00172 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -3,6 +3,12 @@ padding-bottom: 25px; transition: padding $sidebar-transition-duration; + &.page-sidebar-pinned { + .sidebar-wrapper { + @include box-shadow(none); + } + } + .sidebar-wrapper { position: fixed; top: 0; @@ -11,6 +17,7 @@ height: 100%; overflow: hidden; transition: width $sidebar-transition-duration; + @include box-shadow(2px 0 16px 0 $black-transparent); } } @@ -48,10 +55,6 @@ overflow-y: auto; overflow-x: hidden; - @media (min-width: $sidebar-breakpoint) { - bottom: 50px; - } - &.navbar-collapse { padding: 0 !important; } @@ -69,7 +72,7 @@ } a { - padding: 7px 15px 7px 12px; + padding: 7px $gl-sidebar-padding; font-size: $gl-font-size; line-height: 24px; display: block; @@ -101,7 +104,7 @@ } } -.toggle-nav-collapse { +.sidebar-action-buttons { width: $sidebar_width; position: absolute; top: 0; @@ -110,12 +113,37 @@ padding: 5px 0; font-size: 18px; line-height: 30px; + + .toggle-nav-collapse { + left: 0; + } + + .pin-nav-btn { + right: 0; + display: none; + + @media (min-width: $sidebar-breakpoint) { + display: block; + } + + .fa { + transition: transform .15s; + } + + &.is-active { + .fa { + transform: rotate(90deg); + } + } + } } .nav-header-btn { - padding: 10px 5px; + padding: 10px $gl-sidebar-padding; color: inherit; transition-duration: .3s; + position: absolute; + top: 0; &:hover, &:focus { @@ -124,30 +152,6 @@ } } -.pin-nav-btn { - display: none; - position: absolute; - left: 0; - bottom: 0; - height: 50px; - width: $sidebar_width; - line-height: 30px; - - @media (min-width: $sidebar-breakpoint) { - display: block; - } - - .fa { - transition: transform .15s; - } - - &.is-active { - .fa { - transform: rotate(90deg); - } - } -} - .page-sidebar-collapsed { padding-left: 0; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4337fab5d87..f0e7002e4cd 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -17,6 +17,7 @@ $focus-border-color: #3aabf0; $table-border-color: #f0f0f0; $background-color: #fafafa; $dark-background-color: #f7f7f7; +$table-text-gray: #8f8f8f; /* * Text @@ -63,6 +64,7 @@ $gl-btn-padding: 10px; $gl-input-padding: 10px; $gl-vert-padding: 6px; $gl-padding-top: 10px; +$gl-sidebar-padding: 22px; /* * Misc diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 1d34a7f79ae..5607239d92d 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -88,13 +88,7 @@ .user-name { display: inline-block; - font-weight: bold; - } - - .controls { - > .btn, > .dropdown { - margin-left: 5px; - } + font-weight: 600; } .dropdown { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index e8f1935d239..99a2cd306cf 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -83,14 +83,6 @@ } } -table.builds { - .build-link { - a { - color: $gl-dark-link-color; - } - } -} - .build-trace { background: $ci-output-bg; color: $ci-text-color; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 85bbf70e188..0298577c494 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -61,7 +61,7 @@ font-size: 0; } - .btn-transparent { + .btn-clipboard, .btn-transparent { padding-left: 0; padding-right: 0; } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 701b9388454..2a3acc3eb4c 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -38,33 +38,6 @@ margin-right: 15px; } } - - &.group-admin { - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - - .group-avatar, .group-details, .group-controls { - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - } - - .group-details { - flex: 1 1 auto; - flex-direction: column; - min-width: 0; - } - - .group-controls { - align-items: center; - - a { - margin-left: 5px; - } - } - } - } .ldap-group-links { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 4e35ca329e4..0e4d8c140aa 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -63,8 +63,8 @@ form.edit-issue { .merge-request, .issue { &.today { - background: #efe; - border-color: #cec; + background: #f8feef; + border-color: #e1e8d5; } &.closed { diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 47bfd144930..3b1e38fc07d 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -162,9 +162,15 @@ } .filtered-labels { + font-size: 0; + padding: 12px 16px; + .label-row { + margin-top: 4px; + margin-bottom: 4px; + &:not(:last-child) { - margin-right: 5px; + margin-right: 8px; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index d9756b66af0..15c6c9f231a 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -73,11 +73,14 @@ color: #888; } - &.ci-pending, - &.ci-running { + &.ci-pending { color: $gl-warning; } + &.ci-running { + color: $blue-normal; + } + &.ci-failed, &.ci-error { color: $gl-danger; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 6128868b670..a0334207c68 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,15 +1,12 @@ .pipelines { .stage { - max-width: 100px; + max-width: 80px; + width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .duration, .finished_at { - margin: 4px 0; - } - .commit-title { margin: 0; } @@ -22,3 +19,156 @@ margin: 4px; } } + +.content-list { + + &.pipelines, + &.builds-content-list { + width: 100%; + overflow: auto; + } +} + +.table.builds { + min-width: 1100px; + + tr { + th { + padding: 16px; + border: none; + } + } + + tbody { + border-top-width: 1px; + } + + .commit-link { + + a:hover { + text-decoration: none; + } + } + + .branch-commit { + + .branch-name { + margin-left: 8px; + font-weight: bold; + max-width: 180px; + overflow: hidden; + display: inline-block; + white-space: nowrap; + vertical-align: top; + text-overflow: ellipsis; + } + + svg { + margin: 0 6px; + height: 14px; + width: auto; + vertical-align: middle; + } + + .commit-id { + color: $gl-link-color; + margin-right: 8px; + } + + .commit-title { + margin-top: 4px; + max-width: 320px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .avatar { + margin-left: 0; + } + + .label { + margin-right: 4px; + } + + .label-container { + font-size: 0; + + .label { + margin-top: 5px; + } + } + } + + .duration, + .finished-at { + color: $table-text-gray; + margin: 4px 0; + + .fa { + font-size: 12px; + } + + svg { + height: 12px; + width: auto; + vertical-align: middle; + } + + .fa, + svg { + margin-right: 5px; + } + } + + .pipeline-actions { + + .btn { + margin: 0; + color: $table-text-gray; + } + + .cancel-retry-btns { + vertical-align: middle; + + .btn:not(:first-child) { + margin-left: 8px; + } + } + + .dropdown-toggle, + .dropdown-menu { + color: $table-text-gray; + + .fa { + color: $table-text-gray; + margin-right: 6px; + font-size: 14px; + } + } + + .btn-remove { + color: $white-light; + } + + .btn-group { + &.open { + .btn-default { + background-color: $white-normal; + border-color: $border-white-normal; + } + } + } + } + + .build-link { + + a { + color: $gl-dark-link-color; + } + } + + .btn-group.open .dropdown-toggle { + box-shadow: none; + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index bce4aac3334..ea9f7cf0540 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -340,23 +340,30 @@ a.deploy-project-label { .project-import { .form-group { - margin-bottom: 0; + margin-bottom: 5px; } + .import-buttons { padding-left: 0; display: -webkit-flex; display: flex; -webkit-flex-wrap: wrap; flex-wrap: wrap; + .btn { - margin-right: 10px; - padding: 8px 12px; + margin: 0 10px 10px 0; + padding: 8px; } - &> div { - margin-bottom: 14px; + + > div { padding-left: 0; + &:last-child { margin-bottom: 0; + + .btn { + margin-right: 0; + } } } } @@ -475,6 +482,10 @@ pre.light-well { a:hover { text-decoration: none; } + + > span { + margin-left: 10px; + } } } @@ -632,3 +643,9 @@ pre.light-well { width: 300px; } } + +.compare-form-group { + .dropdown-menu { + width: 300px; + } +} diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 9e9b18fdbb8..c9d436d72ba 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -185,7 +185,7 @@ padding-right: $gl-padding + 15px; } - .btn-search { + .btn-search, .btn-new { width: 100%; margin-top: 5px; diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 2370d35924e..c6b053150be 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -32,11 +32,15 @@ border-color: $gl-gray; } - &.ci-pending, - &.ci-running { + &.ci-pending { color: $gl-warning; border-color: $gl-warning; } + + &.ci-running { + color: $blue-normal; + border-color: $blue-normal; + } } .ci-status-icon-success { @@ -45,10 +49,12 @@ .ci-status-icon-failed { color: $gl-danger; } - .ci-status-icon-running, .ci-status-icon-pending { color: $gl-warning; } + .ci-status-icon-running { + color: $blue-normal; + } .ci-status-icon-canceled, .ci-status-icon-disabled, .ci-status-icon-not-found, diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 5b61270daa8..42a20e9775f 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -23,12 +23,11 @@ } &:hover { - cursor: pointer; - td { background-color: $row-hover; border-top: 1px solid $row-hover-border; border-bottom: 1px solid $row-hover-border; + cursor: pointer; } } diff --git a/app/controllers/admin/builds_controller.rb b/app/controllers/admin/builds_controller.rb index 0db91eaaf2e..88f3c0e2fd4 100644 --- a/app/controllers/admin/builds_controller.rb +++ b/app/controllers/admin/builds_controller.rb @@ -5,8 +5,10 @@ class Admin::BuildsController < Admin::ApplicationController @builds = @all_builds.order('created_at DESC') @builds = case @scope + when 'pending' + @builds.pending.reverse_order when 'running' - @builds.running_or_pending.reverse_order + @builds.running.reverse_order when 'finished' @builds.finished else diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9cc31620d9f..a1004d9bcea 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -344,10 +344,6 @@ class ApplicationController < ActionController::Base session[:skip_tfa] && session[:skip_tfa] > Time.current end - def browser_supports_u2f? - browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile? - end - def redirect_to_home_page_url? # If user is not signed-in and tries to access root_path - redirect him to landing page # Don't redirect to the default URL to prevent endless redirections diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index 998b8adc411..ba07cea569c 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -57,7 +57,7 @@ module AuthenticatesWithTwoFactor # Authenticate using the response from a U2F (universal 2nd factor) device def authenticate_with_two_factor_via_u2f(user) - if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenges]) + if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge]) # Remove any lingering user data from login session.delete(:otp_user_id) session.delete(:challenges) @@ -77,11 +77,9 @@ module AuthenticatesWithTwoFactor if key_handles.present? sign_requests = u2f.authentication_requests(key_handles) - challenges = sign_requests.map(&:challenge) - session[:challenges] = challenges - gon.push(u2f: { challenges: challenges, app_id: u2f_app_id, - sign_requests: sign_requests, - browser_supports_u2f: browser_supports_u2f? }) + session[:challenge] ||= u2f.challenge + gon.push(u2f: { challenge: session[:challenge], app_id: u2f_app_id, + sign_requests: sign_requests }) end end end diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb new file mode 100644 index 00000000000..e09b8789eb2 --- /dev/null +++ b/app/controllers/concerns/diff_for_path.rb @@ -0,0 +1,25 @@ +module DiffForPath + extend ActiveSupport::Concern + + def render_diff_for_path(diffs, diff_refs, project) + diff_file = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository).find do |diff| + diff.old_path == params[:old_path] && diff.new_path == params[:new_path] + end + + return render_404 unless diff_file + + diff_commit = commit_for_diff(diff_file) + blob = diff_file.blob(diff_commit) + @expand_all_diffs = true + + locals = { + diff_file: diff_file, + diff_commit: diff_commit, + diff_refs: diff_refs, + blob: blob, + project: project + } + + render json: { html: view_to_html_string('projects/diffs/_content', locals) } + end +end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 3a2db3e6eeb..19a76a5b5d8 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -1,6 +1,4 @@ class Dashboard::TodosController < Dashboard::ApplicationController - include TodosHelper - before_action :find_todos, only: [:index, :destroy_all] def index @@ -13,7 +11,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' } format.js { head :ok } - format.json { render json: { count: todos_pending_count, done_count: todos_done_count } } + format.json { render json: todos_counts } end end @@ -23,7 +21,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } format.js { head :ok } - format.json { render json: { count: todos_pending_count, done_count: todos_done_count } } + format.json { render json: todos_counts } end end @@ -36,4 +34,11 @@ class Dashboard::TodosController < Dashboard::ApplicationController def find_todos @todos ||= TodosFinder.new(current_user, params).execute end + + def todos_counts + { + count: TodosFinder.new(current_user, state: :pending).execute.count, + done_count: TodosFinder.new(current_user, state: :done).execute.count + } + end end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 9b5c43b17e2..d3dd98c8a4e 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -12,13 +12,12 @@ class HelpController < ApplicationController end def show - @category = clean_path_info(path_params[:category]) - @file = path_params[:file] + @path = clean_path_info(path_params[:path]) respond_to do |format| format.any(:markdown, :md, :html) do # Note: We are purposefully NOT using `Rails.root.join` - path = File.join(Rails.root, 'doc', @category, "#{@file}.md") + path = File.join(Rails.root, 'doc', "#{@path}.md") if File.exist?(path) @markdown = File.read(path) @@ -33,7 +32,7 @@ class HelpController < ApplicationController # Allow access to images in the doc folder format.any(:png, :gif, :jpeg) do # Note: We are purposefully NOT using `Rails.root.join` - path = File.join(Rails.root, 'doc', @category, "#{@file}.#{params[:format]}") + path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}") if File.exist?(path) send_file(path, disposition: 'inline') @@ -57,8 +56,7 @@ class HelpController < ApplicationController private def path_params - params.require(:category) - params.require(:file) + params.require(:path) params end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index f35d631df0c..f54c79c2e37 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -107,7 +107,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController # Only allow properly saved users to login. if @user.persisted? && @user.valid? log_audit_event(@user, with: oauth['provider']) - sign_in_and_redirect(@user) + if @user.two_factor_enabled? + prompt_for_two_factor(@user) + else + sign_in_and_redirect(@user) + end else error_message = @user.errors.full_messages.to_sentence diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 6a358fdcc05..e37e9e136db 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -100,7 +100,6 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id, register_requests: registration_requests, - sign_requests: sign_requests, - browser_supports_u2f: browser_supports_u2f? }) + sign_requests: sign_requests }) end end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index bfe0781d735..f33cf238d88 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -23,10 +23,9 @@ class Projects::ArtifactsController < Projects::ApplicationController entry = build.artifacts_metadata_entry(params[:path]) if entry.exists? - render json: { archive: build.artifacts_file.path, - entry: Base64.encode64(entry.path) } + send_artifacts_entry(build, entry) else - render json: {}, status: 404 + render_404 end end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index ef3051d7519..d7513d75f01 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -10,8 +10,10 @@ class Projects::BuildsController < Projects::ApplicationController @builds = @all_builds.order('created_at DESC') @builds = case @scope + when 'pending' + @builds.pending.reverse_order when 'running' - @builds.running_or_pending.reverse_order + @builds.running.reverse_order when 'finished' @builds.finished else diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 37d6521026c..727e84b40a1 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -3,6 +3,7 @@ # Not to be confused with CommitsController, plural. class Projects::CommitController < Projects::ApplicationController include CreatesCommit + include DiffForPath include DiffHelper # Authorize @@ -11,29 +12,14 @@ class Projects::CommitController < Projects::ApplicationController before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds] before_action :authorize_read_commit_status!, only: [:builds] before_action :commit - before_action :define_show_vars, only: [:show, :builds] + before_action :define_commit_vars, only: [:show, :diff_for_path, :builds] + before_action :define_status_vars, only: [:show, :builds] + before_action :define_note_vars, only: [:show, :diff_for_path] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] def show apply_diff_view_cookie! - @grouped_diff_notes = commit.notes.grouped_diff_notes - @notes = commit.notes.non_diff_notes.fresh - - Banzai::NoteRenderer.render( - @grouped_diff_notes.values.flatten + @notes, - @project, - current_user, - ) - - @note = @project.build_commit_note(commit) - - @noteable = @commit - @comments_target = { - noteable_type: 'Commit', - commit_id: @commit.id - } - respond_to do |format| format.html format.diff { render text: @commit.to_diff } @@ -41,6 +27,10 @@ class Projects::CommitController < Projects::ApplicationController end end + def diff_for_path + render_diff_for_path(@diffs, @commit.diff_refs, @project) + end + def builds end @@ -114,7 +104,7 @@ class Projects::CommitController < Projects::ApplicationController @ci_builds ||= Ci::Build.where(pipeline: pipelines) end - def define_show_vars + def define_commit_vars return git_not_found! unless commit opts = diff_options @@ -122,7 +112,28 @@ class Projects::CommitController < Projects::ApplicationController @diffs = commit.diffs(opts) @notes_count = commit.notes.count + end + + def define_note_vars + @grouped_diff_notes = commit.notes.grouped_diff_notes + @notes = commit.notes.non_diff_notes.fresh + + Banzai::NoteRenderer.render( + @grouped_diff_notes.values.flatten + @notes, + @project, + current_user, + ) + + @note = @project.build_commit_note(commit) + + @noteable = @commit + @comments_target = { + noteable_type: 'Commit', + commit_id: @commit.id + } + end + def define_status_vars @statuses = CommitStatus.where(pipeline: pipelines) @builds = Ci::Build.where(pipeline: pipelines) end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index d240b9fe989..5f3ee71444d 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -1,29 +1,51 @@ require 'addressable/uri' class Projects::CompareController < Projects::ApplicationController + include DiffForPath include DiffHelper # Authorize before_action :require_non_empty_project before_action :authorize_download_code! - before_action :assign_ref_vars, only: [:index, :show] + before_action :define_ref_vars, only: [:index, :show, :diff_for_path] + before_action :define_diff_vars, only: [:show, :diff_for_path] before_action :merge_request, only: [:index, :show] def index end def show - compare = CompareService.new. - execute(@project, @head_ref, @project, @start_ref, diff_options) + end + + def diff_for_path + return render_404 unless @compare + + render_diff_for_path(@diffs, @diff_refs, @project) + end + + def create + redirect_to namespace_project_compare_path(@project.namespace, @project, + params[:from], params[:to]) + end + + private - if compare - @commits = Commit.decorate(compare.commits, @project) + def define_ref_vars + @start_ref = Addressable::URI.unescape(params[:from]) + @ref = @head_ref = Addressable::URI.unescape(params[:to]) + end + + def define_diff_vars + @compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref) + + if @compare + @commits = Commit.decorate(@compare.commits, @project) @start_commit = @project.commit(@start_ref) @commit = @project.commit(@head_ref) @base_commit = @project.merge_base_commit(@start_ref, @head_ref) - @diffs = compare.diffs(diff_options) + @diffs = @compare.diffs(diff_options) @diff_refs = Gitlab::Diff::DiffRefs.new( base_sha: @base_commit.try(:sha), start_sha: @start_commit.try(:sha), @@ -35,18 +57,6 @@ class Projects::CompareController < Projects::ApplicationController end end - def create - redirect_to namespace_project_compare_path(@project.namespace, @project, - params[:from], params[:to]) - end - - private - - def assign_ref_vars - @start_ref = Addressable::URI.unescape(params[:from]) - @ref = @head_ref = Addressable::URI.unescape(params[:to]) - end - def merge_request @merge_request ||= @project.merge_requests.opened. find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 5678a4015b6..df659bb8c3b 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -1,5 +1,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController include ToggleSubscriptionAction + include DiffForPath include DiffHelper include IssuableActions include ToggleAwardEmoji @@ -12,6 +13,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] before_action :define_show_vars, only: [:show, :diffs, :commits, :builds] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] + before_action :define_commit_vars, only: [:diffs] + before_action :define_diff_comment_vars, only: [:diffs] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds] # Allow read any merge_request @@ -53,8 +56,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController def show respond_to do |format| - format.html - + format.html { define_discussion_vars } + format.json do render json: @merge_request end @@ -78,35 +81,38 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = @merge_request.merge_request_diff - @commit = @merge_request.diff_head_commit - @base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit - - @comments_target = { - noteable_type: 'MergeRequest', - noteable_id: @merge_request.id - } - - @use_legacy_diff_notes = !@merge_request.support_new_diff_notes? - @grouped_diff_notes = @merge_request.notes.grouped_diff_notes - - Banzai::NoteRenderer.render( - @grouped_diff_notes.values.flatten, - @project, - current_user, - @path, - @project_wiki, - @ref - ) - respond_to do |format| - format.html + format.html { define_discussion_vars } format.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } } end end + # With an ID param, loads the MR at that ID. Otherwise, accepts the same params as #new + # and uses that (unsaved) MR. + # + def diff_for_path + if params[:id] + merge_request + define_diff_comment_vars + else + build_merge_request + @diff_notes_disabled = true + @grouped_diff_notes = {} + end + + define_commit_vars + diffs = @merge_request.diffs(diff_options) + + render_diff_for_path(diffs, @merge_request.diff_refs, @merge_request.project) + end + def commits respond_to do |format| - format.html { render 'show' } + format.html do + define_discussion_vars + + render 'show' + end format.json do # Get commits from repository # or from cache if already merged @@ -121,14 +127,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController def builds respond_to do |format| - format.html { render 'show' } + format.html do + define_discussion_vars + + render 'show' + end format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } } 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 + build_merge_request @noteable = @merge_request @target_branches = if @merge_request.target_project @@ -352,14 +361,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.unlock_mr @merge_request.close end - - if request.format == :html || action_name == 'show' - define_show_html_vars - end end - # Discussion tab data is only required on html requests - def define_show_html_vars + # Discussion tab data is rendered on html responses of actions + # :show, :diff, :commits, :builds. but not when request the data through AJAX + def define_discussion_vars # Build a note object for comment form @note = @project.notes.new(noteable: @noteable) @@ -384,6 +390,30 @@ class Projects::MergeRequestsController < Projects::ApplicationController @pipelines = [@pipeline].compact end + def define_commit_vars + @commit = @merge_request.diff_head_commit + @base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit + end + + def define_diff_comment_vars + @comments_target = { + noteable_type: 'MergeRequest', + noteable_id: @merge_request.id + } + + @use_legacy_diff_notes = !@merge_request.support_new_diff_notes? + @grouped_diff_notes = @merge_request.notes.grouped_diff_notes + + Banzai::NoteRenderer.render( + @grouped_diff_notes.values.flatten, + @project, + current_user, + @path, + @project_wiki, + @ref + ) + end + def invalid_mr # Render special view for MR with removed source or target branch render 'invalid' @@ -412,4 +442,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active? end + + def build_merge_request + params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) + @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute + end end diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb index 23868d986e9..5685d0f4e7c 100644 --- a/app/controllers/projects/todos_controller.rb +++ b/app/controllers/projects/todos_controller.rb @@ -5,7 +5,7 @@ class Projects::TodosController < Projects::ApplicationController todo = TodoService.new.mark_todo(issuable, current_user) render json: { - count: current_user.todos_pending_count, + count: TodosFinder.new(current_user, state: :pending).execute.count, delete_path: dashboard_todo_path(todo) } end diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 7806d9e4cc5..ff866c2faa5 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -8,7 +8,7 @@ # action_id: integer # author_id: integer # project_id; integer -# state: 'pending' or 'done' +# state: 'pending' (default) or 'done' # type: 'Issue' or 'MergeRequest' # @@ -37,7 +37,7 @@ class TodosFinder private def action_id? - action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED, Todo::MARKED].include?(action_id.to_i) + action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i) end def action_id diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 950f323e383..e12a1052988 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -31,7 +31,7 @@ module AppearancesHelper end end - def navbar_icon(icon_name, size: 16) + def custom_icon(icon_name, size: 16) render "shared/icons/#{icon_name}.svg", size: size end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 428a42266d0..abe115d8c68 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,10 +1,7 @@ module BlobHelper - def highlighter(blob_name, blob_content, repository: nil, nowrap: false) - Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap, repository: repository) - end - - def highlight(blob_name, blob_content, repository: nil, nowrap: false, plain: false) - Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap, plain: plain, repository: repository) + def highlight(blob_name, blob_content, repository: nil, plain: false) + highlighted = Gitlab::Highlight.highlight(blob_name, blob_content, plain: plain, repository: repository) + raw %(<pre class="code highlight"><code>#{highlighted}</code></pre>) end def no_highlight_files diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 0f097f86816..b478580978b 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -15,29 +15,13 @@ module ButtonHelper # # See http://clipboardjs.com/#usage def clipboard_button(data = {}) + data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) content_tag :button, icon('clipboard'), class: "btn btn-clipboard", data: data, - type: :button - end - - # Output a "Copy to Clipboard" button with a custom CSS class - # - # data - Data attributes passed to `content_tag` - # css_class - Class passed to the `content_tag` - # - # Examples: - # - # # Define the target element - # clipboard_button_with_class({clipboard_target: "div#foo"}, css_class: "btn-clipboard") - # # => "<button class='btn btn-clipboard' data-clipboard-target='div#foo'>...</button>" - def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard') - content_tag :button, - icon('clipboard'), - class: "btn #{css_class}", - data: data, - type: :button + type: :button, + title: "Copy to Clipboard" end def http_clone_button(project, placement = 'right', append_link: true) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8e4ae1e6aec..e6c99c9959e 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -29,8 +29,10 @@ module CiStatusHelper 'check' when 'failed' 'close' - when 'running', 'pending' + when 'pending' 'clock-o' + when 'running' + 'spinner' else 'circle' end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index eb57516247d..adab901700c 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -8,6 +8,10 @@ module DiffHelper [marked_old_line, marked_new_line] end + def expand_all_diffs? + @expand_all_diffs || params[:expand_all_diffs].present? + end + def diff_view diff_views = %w(inline parallel) @@ -18,16 +22,14 @@ module DiffHelper end end - def diff_hard_limit_enabled? - params[:force_show_diff].present? - end - def diff_options - options = { ignore_whitespace_change: hide_whitespace? } - if diff_hard_limit_enabled? - options.merge!(Commit.max_diff_options) + default_options = Commit.max_diff_options + + if action_name == 'diff_for_path' + default_options[:paths] = params.values_at(:old_path, :new_path) end - options + + default_options.merge(ignore_whitespace_change: hide_whitespace?) end def safe_diff_files(diffs, diff_refs: nil, repository: nil) @@ -35,7 +37,7 @@ module DiffHelper end def unfold_bottom_class(bottom) - bottom ? 'js-unfold-bottom' : '' + bottom ? 'js-unfold js-unfold-bottom' : '' end def unfold_class(unfold) @@ -90,7 +92,7 @@ module DiffHelper def commit_for_diff(diff_file) return diff_file.content_commit if diff_file.content_commit - + if diff_file.deleted_file @base_commit || @commit.parent || @commit else diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 294b7e92b8d..47d174361db 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -61,7 +61,7 @@ module IssuablesHelper output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier" output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe output << content_tag(:strong) do - author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs") + author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true) author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg") end end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 4da1f4865a4..db6e731c744 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -96,4 +96,8 @@ module MergeRequestsHelper ["#{source_path}:#{source_branch}", "#{target_path}:#{target_branch}"] end end + + def merge_request_button_visibility(merge_request, closed) + return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) + end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 2302e65c537..98143dcee9b 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -24,7 +24,15 @@ module NotesHelper }.to_json end - def link_to_new_diff_note(line_code, position, line_type = nil) + def diff_view_data + return {} unless @comments_target + + @comments_target.slice(:noteable_id, :noteable_type, :commit_id) + end + + def diff_view_line_data(line_code, position, line_type) + return if @diff_notes_disabled + use_legacy_diff_note = @use_legacy_diff_notes # If the controller doesn't force the use of legacy diff notes, we # determine this on a line-by-line basis by seeing if there already exist @@ -41,11 +49,8 @@ module NotesHelper end data = { - noteable_type: @comments_target[:noteable_type], - noteable_id: @comments_target[:noteable_id], - commit_id: @comments_target[:commit_id], - line_type: line_type, - line_code: line_code + line_code: line_code, + line_type: line_type, } if use_legacy_diff_note @@ -73,11 +78,7 @@ module NotesHelper ) end - 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 + data end def link_to_reply_discussion(note, line_type = nil) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 3bbbb26cff2..a733dff1579 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -19,7 +19,7 @@ module ProjectsHelper end def link_to_member(project, author, opts = {}, &block) - default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" } + default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name", tooltip: false } opts = default_opts.merge(opts) return "(deleted)" unless author @@ -33,7 +33,8 @@ module ProjectsHelper if opts[:by_username] author_html << content_tag(:span, sanitize("@#{author.username}"), class: opts[:author_class]) if opts[:name] else - author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name] + tooltip_data = { placement: 'top' } + author_html << content_tag(:span, sanitize(author.name), class: [opts[:author_class], ('has-tooltip' if opts[:tooltip])], title: (author.to_reference if opts[:tooltip]), data: (tooltip_data if opts[:tooltip])) if opts[:name] end author_html << capture(&block) if block @@ -60,7 +61,7 @@ module ProjectsHelper project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } if current_user - project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) + project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) end full_title = "#{namespace_link} / #{project_link}".html_safe diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index f9fc525df6f..fcb2703e837 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -43,15 +43,15 @@ module SearchHelper # Autocomplete results for internal help pages def help_autocomplete [ - { category: "Help", label: "API Help", url: help_page_path("api", "README") }, - { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") }, - { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") }, - { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") }, - { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") }, - { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") }, - { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, - { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, - { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") }, + { category: "Help", label: "API Help", url: help_page_path("api/README") }, + { category: "Help", label: "Markdown Help", url: help_page_path("markdown/markdown") }, + { category: "Help", label: "Permissions Help", url: help_page_path("user/permissions") }, + { category: "Help", label: "Public Access Help", url: help_page_path("public_access/public_access") }, + { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") }, + { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") }, + { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") }, + { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks/web_hooks") }, + { category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") }, ] end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index a832a6c8df7..e3a208f826a 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -1,10 +1,10 @@ module TodosHelper def todos_pending_count - TodosFinder.new(current_user, state: :pending).execute.count + @todos_pending_count ||= TodosFinder.new(current_user, state: :pending).execute.count end def todos_done_count - TodosFinder.new(current_user, state: :done).execute.count + @todos_done_count ||= TodosFinder.new(current_user, state: :done).execute.count end def todo_action_name(todo) @@ -13,6 +13,7 @@ module TodosHelper when Todo::MENTIONED then 'mentioned you on' when Todo::BUILD_FAILED then 'The build failed for your' when Todo::MARKED then 'added a todo for' + when Todo::APPROVAL_REQUIRED then 'set you as an approver for' end end diff --git a/app/helpers/u2f_helper.rb b/app/helpers/u2f_helper.rb new file mode 100644 index 00000000000..143b4ca6b51 --- /dev/null +++ b/app/helpers/u2f_helper.rb @@ -0,0 +1,5 @@ +module U2fHelper + def inject_u2f_api? + browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile? + end +end diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb index 65598ad9ed3..d887cdadc34 100644 --- a/app/helpers/workhorse_helper.rb +++ b/app/helpers/workhorse_helper.rb @@ -28,4 +28,10 @@ module WorkhorseHelper headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) head :ok end + + # Send an entry from artifacts through Workhorse + def send_artifacts_entry(build, entry) + headers.store(*Gitlab::Workhorse.send_artifacts_entry(build, entry)) + head :ok + end end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 236b6ab00d8..e0af7081411 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -29,7 +29,8 @@ module Emails # used in notify layout @target_url = @message.target_url @project = Project.find(project_id) - + @diff_notes_disabled = true + add_project_headers headers['X-GitLab-Author'] = @message.author_username diff --git a/app/models/ability.rb b/app/models/ability.rb index eeb0ceba081..6fd18f2ee24 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -204,7 +204,8 @@ class Ability :download_code, :fork_project, :read_commit_status, - :read_pipeline + :read_pipeline, + :read_container_image ] end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e189dbac285..ffac3a22efc 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -5,6 +5,7 @@ module Ci belongs_to :erased_by, class_name: 'User' serialize :options + serialize :yaml_variables validates :coverage, numericality: true, allow_blank: true validates_presence_of :ref @@ -52,7 +53,10 @@ module Ci new_build.stage = build.stage new_build.stage_idx = build.stage_idx new_build.trigger_request = build.trigger_request + new_build.yaml_variables = build.yaml_variables + new_build.when = build.when new_build.user = user + new_build.environment = build.environment new_build.save MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) new_build @@ -117,7 +121,12 @@ module Ci end def variables - predefined_variables + yaml_variables + project_variables + trigger_variables + variables = [] + variables += predefined_variables + variables += yaml_variables if yaml_variables + variables += project_variables + variables += trigger_variables + variables end def merge_request @@ -394,30 +403,6 @@ module Ci self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil) end - def yaml_variables - global_yaml_variables + job_yaml_variables - end - - def global_yaml_variables - if pipeline.config_processor - pipeline.config_processor.global_variables.map do |key, value| - { key: key, value: value, public: true } - end - else - [] - end - end - - def job_yaml_variables - if pipeline.config_processor - pipeline.config_processor.job_variables(name).map do |key, value| - { key: key, value: value, public: true } - end - else - [] - end - end - def project_variables project.variables.map do |variable| { key: variable.key, value: variable.value, public: false } diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 431a91004cd..a8e6a23e1c4 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -6,6 +6,8 @@ module Ci self.table_name = 'ci_commits' belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id + belongs_to :user + has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id @@ -223,6 +225,8 @@ module Ci end def keep_around_commits + return unless project + project.repository.keep_around(self.sha) project.repository.keep_around(self.before_sha) end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 06beff177b1..800a16ab246 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -65,8 +65,7 @@ module Awardable def create_award_emoji(name, current_user) return unless emoji_awardable? - - award_emoji.create(name: name, user: current_user) + award_emoji.create(name: normalize_name(name), user: current_user) end def remove_award_emoji(name, current_user) @@ -80,4 +79,10 @@ module Awardable create_award_emoji(emoji_name, current_user) end end + + private + + def normalize_name(name) + Gitlab::AwardEmoji.normalize_emoji_name(name) + end end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 8cac47246db..ec9e0f1b1d0 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -14,14 +14,14 @@ module Mentionable attr = attr.to_s mentionable_attrs << [attr, options] end + end + included do # Accessor for attributes marked mentionable. - def mentionable_attrs - @mentionable_attrs ||= [] + cattr_accessor :mentionable_attrs, instance_accessor: false do + [] end - end - included do if self < Participable participant -> (user, ext) { all_references(user, extractor: ext) } end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 9822844357d..70740c76e43 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -41,9 +41,12 @@ module Participable def participant(attr) participant_attrs << attr end + end - def participant_attrs - @participant_attrs ||= [] + included do + # Accessor for participant attributes. + cattr_accessor :participant_attrs, instance_accessor: false do + [] end end diff --git a/app/models/label.rb b/app/models/label.rb index dc5586f5756..35e678001dc 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -52,14 +52,17 @@ class Label < ActiveRecord::Base # This pattern supports cross-project references. # def self.reference_pattern + # NOTE: The id pattern only matches when all characters on the expression + # are digits, so it will match ~2 but not ~2fa because that's probably a + # label name and we want it to be matched as such. @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)} (?: - (?<label_id>\d+) | # Integer-based label ID, or + (?<label_id>\d+(?!\S\w)\b) | # 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 + [A-Za-z0-9_\-\?\.&]+ | # String-based single-word label title, or + ".+?" # String-based multi-word label surrounded in quotes ) ) }x diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 790dfd4d480..04a651d50ab 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -38,7 +38,7 @@ class LegacyDiffNote < Note end def diff_line - @diff_line ||= diff_file.line_for_line_code(self.line_code) + @diff_line ||= diff_file.line_for_line_code(self.line_code) if diff_file end def for_line?(line) @@ -55,7 +55,7 @@ class LegacyDiffNote < Note def active? return @active if defined?(@active) return true if for_commit? - return true unless self.diff + return true unless diff_line return false unless noteable noteable_diff = find_noteable_diff diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 393d8a72657..157901378d3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -19,7 +19,7 @@ class MergeRequest < ActiveRecord::Base after_create :create_merge_request_diff, unless: :importing? after_update :update_merge_request_diff - delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil + delegate :commits, :real_size, to: :merge_request_diff, prefix: nil # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests @@ -164,6 +164,10 @@ class MergeRequest < ActiveRecord::Base merge_request_diff ? merge_request_diff.first_commit : compare_commits.first end + def diffs(*args) + merge_request_diff ? merge_request_diff.diffs(*args) : compare.diffs(*args) + end + def diff_size merge_request_diff.size end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index ba235750aeb..feaba925bad 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -46,7 +46,8 @@ class MergeRequestDiff < ActiveRecord::Base compare.diffs(options) end else - @diffs ||= load_diffs(st_diffs, options) + @diffs ||= {} + @diffs[options] ||= load_diffs(st_diffs, options) end end @@ -144,6 +145,12 @@ class MergeRequestDiff < ActiveRecord::Base def load_diffs(raw, options) if raw.respond_to?(:each) + if paths = options[:paths] + raw = raw.select do |diff| + paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) + end + end + Gitlab::Git::DiffCollection.new(raw, options) else Gitlab::Git::DiffCollection.new([]) diff --git a/app/models/note.rb b/app/models/note.rb index ffffd0c0838..0ce10c77de9 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -10,6 +10,10 @@ class Note < ActiveRecord::Base # Banzai::ObjectRenderer. attr_accessor :note_html + # An Array containing the number of visible references as generated by + # Banzai::ObjectRenderer + attr_accessor :user_visible_reference_count + default_value_for :system, false attr_mentionable :note, pipeline: :note @@ -193,7 +197,15 @@ class Note < ActiveRecord::Base end def cross_reference_not_visible_for?(user) - cross_reference? && referenced_mentionables(user).empty? + cross_reference? && !has_referenced_mentionables?(user) + end + + def has_referenced_mentionables?(user) + if user_visible_reference_count.present? + user_visible_reference_count > 0 + else + referenced_mentionables(user).any? + end end def award_emoji? @@ -217,8 +229,7 @@ class Note < ActiveRecord::Base end def award_emoji_name - original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] - Gitlab::AwardEmoji.normalize_emoji_name(original_name) + note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] end private diff --git a/app/models/project.rb b/app/models/project.rb index d26aa8073e8..12851c5d0ec 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -470,8 +470,8 @@ class Project < ActiveRecord::Base return super(value) unless Gitlab::UrlSanitizer.valid?(value) import_url = Gitlab::UrlSanitizer.new(value) - create_or_update_import_data(credentials: import_url.credentials) super(import_url.sanitized_url) + create_or_update_import_data(credentials: import_url.credentials) end def import_url @@ -483,7 +483,13 @@ class Project < ActiveRecord::Base end end + def valid_import_url? + valid? || errors.messages[:import_url].nil? + end + def create_or_update_import_data(data: nil, credentials: nil) + return unless import_url.present? && valid_import_url? + project_import_data = import_data || build_import_data if data project_import_data.data ||= {} @@ -1038,8 +1044,8 @@ class Project < ActiveRecord::Base pipelines.order(id: :desc).find_by(sha: sha, ref: ref) end - def ensure_pipeline(sha, ref) - pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref) + def ensure_pipeline(sha, ref, current_user = nil) + pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user) end def enable_ci diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index 016172c6d7e..f4bcb49b34d 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -72,6 +72,19 @@ class SentNotification < ActiveRecord::Base end end + def position=(new_position) + if new_position.is_a?(String) + new_position = JSON.parse(new_position) rescue nil + end + + if new_position.is_a?(Hash) + new_position = new_position.with_indifferent_access + new_position = Gitlab::Diff::Position.new(new_position) + end + + super(new_position) + end + def to_param self.reply_key end diff --git a/app/models/todo.rb b/app/models/todo.rb index ac3fdbc7f3b..8d7a5965aa1 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -1,14 +1,16 @@ class Todo < ActiveRecord::Base - ASSIGNED = 1 - MENTIONED = 2 - BUILD_FAILED = 3 - MARKED = 4 + ASSIGNED = 1 + MENTIONED = 2 + BUILD_FAILED = 3 + MARKED = 4 + APPROVAL_REQUIRED = 5 # This is an EE-only feature ACTION_NAMES = { ASSIGNED => :assigned, MENTIONED => :mentioned, BUILD_FAILED => :build_failed, - MARKED => :marked + MARKED => :marked, + APPROVAL_REQUIRED => :approval_required } belongs_to :author, class_name: "User" diff --git a/app/models/user.rb b/app/models/user.rb index 79c670cb35a..3d0a033785c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -85,9 +85,10 @@ class User < ActiveRecord::Base has_one :abuse_report, dependent: :destroy has_many :spam_logs, dependent: :destroy has_many :builds, dependent: :nullify, class_name: 'Ci::Build' + has_many :pipelines, dependent: :nullify, class_name: 'Ci::Pipeline' has_many :todos, dependent: :destroy has_many :notification_settings, dependent: :destroy - has_many :award_emoji, as: :awardable, dependent: :destroy + has_many :award_emoji, dependent: :destroy # # Validations diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 2dcb052d274..3b21f0acb96 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -36,7 +36,9 @@ module Ci :allow_failure, :stage, :stage_idx, - :environment) + :environment, + :when, + :yaml_variables) build_attrs.merge!(pipeline: @pipeline, ref: @pipeline.ref, diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index b1ee6874190..be91bf0db85 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -2,6 +2,7 @@ module Ci class CreatePipelineService < BaseService def execute pipeline = project.pipelines.new(params) + pipeline.user = current_user unless ref_names.include?(params[:ref]) pipeline.errors.add(:base, 'Reference not found') diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index e2bccbdbcc3..149822aa647 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,7 +3,7 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::Git::Compare object that responds to commits and diffs class CompareService - def execute(source_project, source_branch, target_project, target_branch, diff_options = {}) + def execute(source_project, source_branch, target_project, target_branch) source_commit = source_project.commit(source_branch) return unless source_commit diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index f947e8f452e..0b66b854dea 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -14,7 +14,13 @@ class CreateCommitBuildsService return false end - @pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag) + @pipeline = Ci::Pipeline.new( + project: project, + sha: sha, + ref: ref, + before_sha: before_sha, + tag: tag, + user: user) ## # Skip creating pipeline if no gitlab-ci.yml is found diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 02fca5c0ea3..18971bd0be3 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -8,7 +8,6 @@ module Notes if note.award_emoji? noteable = note.noteable todo_service.new_award_emoji(noteable, current_user) - return noteable.create_award_emoji(note.award_emoji_name, current_user) end diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index 752c11d7ae6..29b3981f49f 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -7,8 +7,6 @@ # module Projects class HousekeepingService < BaseService - include Gitlab::ShellAdapter - LEASE_TIMEOUT = 3600 class LeaseTaken < StandardError @@ -24,11 +22,7 @@ module Projects def execute raise LeaseTaken unless try_obtain_lease - GitlabShellOneShotWorker.perform_async(:gc, @project.repository_storage_path, @project.path_with_namespace) - ensure - Gitlab::Metrics.measure(:reset_pushes_since_gc) do - update_pushes_since_gc(0) - end + execute_gitlab_shell_gc end def needed? @@ -36,19 +30,27 @@ module Projects end def increment! - Gitlab::Metrics.measure(:increment_pushes_since_gc) do - update_pushes_since_gc(@project.pushes_since_gc + 1) + if Gitlab::ExclusiveLease.new("project_housekeeping:increment!:#{@project.id}", timeout: 60).try_obtain + Gitlab::Metrics.measure(:increment_pushes_since_gc) do + update_pushes_since_gc(@project.pushes_since_gc + 1) + end end end private - def update_pushes_since_gc(new_value) - if Gitlab::ExclusiveLease.new("project_housekeeping:update_pushes_since_gc:#{project.id}", timeout: 60).try_obtain - @project.update_column(:pushes_since_gc, new_value) + def execute_gitlab_shell_gc + GitGarbageCollectWorker.perform_async(@project.id) + ensure + Gitlab::Metrics.measure(:reset_pushes_since_gc) do + update_pushes_since_gc(0) end end + def update_pushes_since_gc(new_value) + @project.update_column(:pushes_since_gc, new_value) + end + def try_obtain_lease Gitlab::Metrics.measure(:obtain_housekeeping_lease) do lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 6afc048576d..998789d64d2 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -10,7 +10,7 @@ module Projects def save_all if [version_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save) - Gitlab::ImportExport::Saver.save(shared: @shared) + Gitlab::ImportExport::Saver.save(project: project, shared: @shared) notify_success else cleanup_and_notify diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 163ebf26c84..cdad0426b02 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -43,7 +43,7 @@ module Projects def import_repository begin gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url) - rescue Gitlab::Shell::Error => e + rescue => e raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" end end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 6bb0a72d30e..6b48d68cccb 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -194,7 +194,7 @@ class TodoService end def create_assignment_todo(issuable, author) - if issuable.assignee && issuable.assignee != author + if issuable.assignee attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED) create_todos(issuable.assignee, attributes) end @@ -239,7 +239,6 @@ class TodoService def filter_mentioned_users(project, target, author) mentioned_users = target.mentioned_users(author) mentioned_users = reject_users_without_access(mentioned_users, project, target) - mentioned_users.delete(author) mentioned_users.uniq end diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index dc083e50178..92e2dae4842 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -13,7 +13,7 @@ .col-sm-10 = f.text_area :description, class: "form-control", rows: 10 .hint - Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown', 'markdown'), target: '_blank'}. + Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown/markdown'), target: '_blank'}. .form-group = f.label :logo, class: 'control-label' .col-sm-10 diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 8de28528cda..538d8176ce7 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -38,11 +38,11 @@ = source %span.help-block#import-sources-help Enabled sources for code import during project creation. OmniAuth must be configured for GitHub - = link_to "(?)", help_page_path("integration", "github") + = link_to "(?)", help_page_path("integration/github") , Bitbucket - = link_to "(?)", help_page_path("integration", "bitbucket") + = link_to "(?)", help_page_path("integration/bitbucket") and GitLab.com - = link_to "(?)", help_page_path("integration", "gitlab") + = link_to "(?)", help_page_path("integration/gitlab") .form-group %label.control-label.col-sm-2 Enabled Git access protocols .col-sm-10 diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index 967151bc33b..ce818c30c30 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -1,30 +1,40 @@ - project = build.project -%tr.build +%tr.build.commit %td.status = ci_status_with_icon(build.status) - %td.build-link - - if can?(current_user, :read_build, build.project) - = link_to namespace_project_build_url(build.project.namespace, build.project, build) do - %strong Build ##{build.id} - - else - %strong Build ##{build.id} + %td + .branch-commit + - if can?(current_user, :read_build, build.project) + = link_to namespace_project_build_url(build.project.namespace, build.project, build) do + %span.build-link ##{build.id} + - else + %span.build-link ##{build.id} - - if build.stuck? - %i.fa.fa-warning.text-warning + - if build.stuck? + %i.fa.fa-warning.text-warning - %td - - if project - = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project) + - if build.ref + = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" + - else + .light none + = custom_icon("icon_commit") - %td - = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace" + = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace commit-id" + + .label-container + - if build.tags.any? + - build.tags.each do |tag| + %span.label.label-primary + = tag + - if build.try(:trigger_request) + %span.label.label-info triggered + - if build.try(:allow_failure) + %span.label.label-danger allowed to fail %td - - if build.ref - = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref) - - else - .light none + - if project + = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project) %td - if build.try(:runner) @@ -36,22 +46,15 @@ #{build.stage} / #{build.name} %td - - if build.tags.any? - - build.tags.each do |tag| - %span.label.label-primary - = tag - - if build.try(:trigger_request) - %span.label.label-info triggered - - if build.try(:allow_failure) - %span.label.label-danger allowed to fail - - %td.duration - if build.duration - #{duration_in_words(build.finished_at, build.started_at)} + %p.duration + = custom_icon("icon_timer") + = duration_in_numbers(build.finished_at, build.started_at) - %td.timestamp - if build.finished_at - %span #{time_ago_with_tooltip(build.finished_at)} + %p.finished-at + = icon("calendar") + %span #{time_ago_with_tooltip(build.finished_at)} - if defined?(coverage) && coverage %td.coverage diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index 1e60205f91a..3d77634d8fa 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -10,15 +10,20 @@ All %span.badge.js-totalbuilds-count= @all_builds.count(:id) + %li{class: ('active' if @scope == 'pending')} + = link_to admin_builds_path(scope: :pending) do + Pending + %span.badge= number_with_delimiter(@all_builds.pending.count(:id)) + %li{class: ('active' if @scope == 'running')} = link_to admin_builds_path(scope: :running) do Running - %span.badge.js-running-count= number_with_delimiter(@all_builds.running_or_pending.count(:id)) + %span.badge= number_with_delimiter(@all_builds.running.count(:id)) %li{class: ('active' if @scope == 'finished')} = link_to admin_builds_path(scope: :finished) do Finished - %span.badge.js-running-count= number_with_delimiter(@all_builds.finished.count(:id)) + %span.badge= number_with_delimiter(@all_builds.finished.count(:id)) .nav-controls - if @all_builds.running_or_pending.any? @@ -27,7 +32,7 @@ .row-content-block.second-block #{(@scope || 'all').capitalize} builds - %ul.content-list + %ul.content-list.builds-content-list - if @builds.blank? %li .nothing-here-block No builds to show @@ -37,15 +42,11 @@ %thead %tr %th Status - %th Build ID - %th Project %th Commit - %th Ref + %th Project %th Runner %th Name - %th Tags - %th Duration - %th Finished at + %th %th - @builds.each do |build| diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml index 15aa059c93d..5c410a695bf 100644 --- a/app/views/admin/deploy_keys/new.html.haml +++ b/app/views/admin/deploy_keys/new.html.haml @@ -14,7 +14,7 @@ .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") + = link_to "here", help_page_path("ssh/README") = f.text_area :key, class: "form-control thin_area", rows: 5 .form-actions diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 59fd6c3fea0..77a11e49e20 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -1,20 +1,26 @@ -- css_class = '' unless local_assigns[:css_class] +- css_class = 'no-description' if group.description.blank? -%li.group-row.group-admin{ class: css_class } - .group-avatar - = image_tag group_icon(group), class: 'avatar hidden-xs' - .group-details - .title - = link_to [:admin, group], class: 'group-name' do - = group.name - .group-stats - %span>= pluralize(number_with_delimiter(group.projects.count), 'project') - , - %span= pluralize(number_with_delimiter(group.users.count), 'member') - - - if group.description.present? - .description - = markdown(group.description, pipeline: :description) - .group-controls.hidden-xs +%li.group-row{ class: css_class } + .controls = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn' = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove' + .stats + %span + = icon('bookmark') + = number_with_delimiter(group.projects.count) + + %span + = icon('users') + = number_with_delimiter(group.users.count) + + %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} + = visibility_level_icon(group.visibility_level, fw: false) + + = image_tag group_icon(group), class: "avatar s40 hidden-xs" + .title + = link_to [:admin, group], class: 'group-name' do + = group.name + + - if group.description.present? + .description + = markdown(group.description, pipeline: :description) diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 522153b37e3..bb374694400 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -79,7 +79,7 @@ .panel-body.form-holder %p.light Read more about project permissions - %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do %div diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index 7b388cf7862..c217490963f 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -3,7 +3,7 @@ System hooks %p.light - #{link_to "System hooks ", help_page_path("system_hooks", "system_hooks"), class: "vlink"} can be + #{link_to "System hooks ", help_page_path("system_hooks/system_hooks"), class: "vlink"} can be used for binding events when GitLab creates a User or Project. %hr diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 7fbce25b2c4..1e755785d90 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -66,7 +66,7 @@ %ul.projects-list.content-list - @projects.each_with_index do |project| %li.project-row - .controls.pull-right + .controls - if project.archived %span.label.label-warning archived %span.label.label-gray diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 2c5aba71699..b2c607361b3 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -132,7 +132,7 @@ - else passed. - = link_to icon('question-circle'), help_page_path('administration', 'repository_checks') + = link_to icon('question-circle'), help_page_path('administration/repository_checks') .form-group = f.submit 'Trigger repository check', class: 'btn btn-primary' diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index d3519f616f6..4bf1c9cde3c 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -14,7 +14,7 @@ %span It's you! .user-email = mail_to user.email, user.email - .controls.pull-right + .controls = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn' - unless user == current_user .dropdown.inline diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index d54c7cad7be..fdea834ff45 100644 --- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml @@ -1,53 +1,46 @@ - publicish_project_count = ProjectsFinder.new.execute(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 - .dashboard-intro-text - %p.slead - You don't have access to any projects right now. - %br - - if current_user.can_create_project? - You can create up to - %strong= pluralize(number_with_delimiter(current_user.projects_limit), "project") + "." - - else - If you are added to a project, it will be displayed here. - +.blank-state.blank-state-welcome + %h2.blank-state-welcome-title + Welcome to GitLab + %p.blank-state-text + Code, test, and deploy together +.blank-state + .blank-state-icon + = custom_icon("project", size: 50) + %h3.blank-state-title + You don't have access to any projects right now + %p.blank-state-text - if current_user.can_create_project? - .link_holder - = link_to new_project_path, class: "btn btn-new" do - = icon('plus') - New Project + You can create up to + %strong= number_with_delimiter(current_user.projects_limit) + = succeed "." do + = "project".pluralize(current_user.projects_limit) + - else + If you are added to a project, it will be displayed here. + - if current_user.can_create_project? + = link_to new_project_path, class: "btn btn-new" do + New project - if current_user.can_create_group? - %hr - %div - .dashboard-intro-icon - %i.fa.fa-users - .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 - %i.fa.fa-plus - New Group + .blank-state + .blank-state-icon + = custom_icon("group", size: 50) + %h3.blank-state-title + You can create a group for several dependent projects. + %p.blank-state-text + Groups are the best way to manage projects and members. + = link_to new_group_path, class: "btn btn-new" do + New group -if publicish_project_count > 0 - %hr - %div - .dashboard-intro-icon - %i.fa.fa-globe - .dashboard-intro-text - %p.slead - There are - %strong= number_with_delimiter(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 + .blank-state + .blank-state-icon + = icon("globe") + %h3.blank-state-title + There are + = number_with_delimiter(publicish_project_count) + public projects on this server. + %p.blank-state-text + Public projects are an easy way to allow everyone to have read-only access. + = link_to trending_explore_projects_path, class: "btn btn-new" do + Browse projects diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 4565e752c1f..4f36a4a1c73 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -5,7 +5,8 @@ - page_title "Projects" - header_title "Projects", dashboard_projects_path -= render 'dashboard/projects_head' +- if @projects.any? || params[:filter_projects] + = render 'dashboard/projects_head' - if @last_push = render "events/event_last_push", event: @last_push diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index fc42e5dcc66..4e340b6ec16 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -9,14 +9,14 @@ %span To do %span.badge - = todos_pending_count + = number_with_delimiter(todos_pending_count) - todo_done_active = ('active' if params[:state] == 'done') %li{class: "todos-done #{todo_done_active}"} = link_to todos_filter_path(state: 'done') do %span Done %span.badge - = todos_done_count + = number_with_delimiter(todos_done_count) .nav-controls - if @todos.any?(&:pending?) diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index a373f61bd3c..4debd3d608f 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -1,3 +1,7 @@ +- if inject_u2f_api? + - content_for :page_specific_javascripts do + = page_specific_javascript_tag('u2f.js') + %div .login-box .login-heading diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index 012e9857642..c034bbe430e 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -3,4 +3,4 @@ %h3 Access Denied %hr %p You are not allowed to access this page. -%p Read more about project permissions #{link_to "here", help_page_path("permissions", "permissions"), class: "vlink"} +%p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"} diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index e7ab4f2409b..9bb9f962177 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -12,7 +12,7 @@ = 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" + %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" .form-actions = f.submit 'Add users to group', class: "btn btn-create" diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 121a7de3ad7..a8fdbd8c426 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -6,7 +6,6 @@ .nav-controls - if can?(current_user, :admin_milestones, @group) = link_to new_group_milestone_path(@group), class: "btn btn-new" do - = icon('plus') New Milestone .row-content-block diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index c2f2d9912f7..33fee334d93 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -7,7 +7,6 @@ - if can? current_user, :admin_group, @group .controls = link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do - = icon('plus') New Project %ul.well-list - @projects.each do |project| diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index a83eb7e88bb..eddeae98bc4 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -6,8 +6,7 @@ .cover-block.groups-cover-block %div{ class: container_class } - = link_to group_icon(@group), target: '_blank' do - = image_tag group_icon(@group), class: "avatar group-avatar s70" + = image_tag group_icon(@group), class: "avatar group-avatar s70" .group-info .cover-title %h1 diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 8cc0b59edeb..ce4536ebdc6 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -20,6 +20,10 @@ %td Focus Search %tr %td.shortcut + .key f + %td Focus Filter + %tr + %td.shortcut .key ? %td Show/hide this dialog %tr diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml index 0398afb4c1d..be257b51b9e 100644 --- a/app/views/help/show.html.haml +++ b/app/views/help/show.html.haml @@ -1,3 +1,3 @@ -- page_title @file.humanize, *@category.split("/").reverse.map(&:humanize) +- page_title @path.split("/").reverse.map(&:humanize) .documentation.wiki = markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com") diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index d676bc28c89..431d312b4ca 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -549,4 +549,4 @@ %li wiki page %li help page - You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown", "markdown")}. + You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown/markdown")}. diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml index 435ed7bd4cb..4c6af0b7908 100644 --- a/app/views/import/github/new.html.haml +++ b/app/views/import/github/new.html.haml @@ -38,6 +38,6 @@ As an administrator you may like to configure - else Consider asking your GitLab administrator to configure - = link_to 'GitHub integration', help_page_path("integration", "github") + = link_to 'GitHub integration', help_page_path("integration/github") which will allow login via GitHub and allow importing projects without generating a Personal Access Token. diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml deleted file mode 100644 index 8c140a5943e..00000000000 --- a/app/views/layouts/_collapse_button.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -= link_to '#', class: 'nav-header-btn text-center toggle-nav-collapse', title: "Open/Close" do - %span.sr-only Toggle navigation - = icon('bars') diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml index cc8ea066cb9..3612f1ce5c6 100644 --- a/app/views/layouts/_flash.html.haml +++ b/app/views/layouts/_flash.html.haml @@ -1,4 +1,4 @@ -.flash-container +.flash-container.flash-container-page - if alert .flash-alert = alert diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 8596bbfdef6..a1a71c2fb33 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,6 +1,13 @@ .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } - = render partial: 'layouts/collapse_button' + .sidebar-action-buttons + = link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do + %span.sr-only Toggle navigation + = icon('bars') + = link_to '#', class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do + %span.sr-only Toggle navigation pinning + = icon('fw thumb-tack') + - if defined?(sidebar) && sidebar = render "layouts/nav/#{sidebar}" - elsif current_user @@ -8,9 +15,6 @@ - else = render 'layouts/nav/explore' - = link_to '#', class: "nav-header-btn text-center pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do - %span.sr-only Toggle navigation pinning - = icon('thumb-tack') - if defined?(nav) && nav .layout-nav .container-fluid diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 245b9c3b4d4..f7580f00159 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -44,7 +44,7 @@ name: "#{j(@project.name)}" }; - - if @group and @group.path + - if @group && @group.persisted? && @group.path :javascript gl.groupOptions = gl.groupOptions || {}; gl.groupOptions["#{j(@group.path)}"] = { diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 11cee421a99..94c53882623 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,7 +1,7 @@ %header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } %div{ class: fluid_layout ? "container-fluid" : "container-fluid" } .header-content - %button.side-nav-toggle{type: 'button'} + %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" } %span.sr-only Toggle navigation = icon('bars') %button.navbar-toggle{type: 'button'} @@ -13,25 +13,25 @@ %li.hidden-sm.hidden-xs = render 'layouts/search' unless current_controller?(:search) %li.visible-sm.visible-xs - = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('search') - if current_user - if session[:impersonator_id] %li.impersonation - = link_to admin_impersonation_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do + = link_to admin_impersonation_path, method: :delete, title: "Stop Impersonation", aria: { label: 'Stop Impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('user-secret fw') - if current_user.is_admin? %li - = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to admin_root_path, title: 'Admin Area', aria: { label: "Admin Area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('wrench fw') %li - = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('bell fw') %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) } = todos_pending_count - if current_user.can_create_project? %li - = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('plus fw') - if Gitlab::Sherlock.enabled? %li @@ -45,12 +45,12 @@ .dropdown-menu-nav.dropdown-menu-align-right %ul %li - = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } + = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } %li - = link_to "Profile Settings", profile_path + = link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" } %li.divider %li - = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", title: 'Sign out' + = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" } - else %li %div diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 52e41b1a857..21668698814 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,64 +1,44 @@ %ul.nav.nav-sidebar = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - .icon-container - = navbar_icon('project') %span Projects = nav_link(controller: :todos) do = link_to dashboard_todos_path, title: 'Todos' do - .icon-container - = icon('bell fw') %span Todos %span.count= number_with_delimiter(todos_pending_count) = nav_link(path: 'dashboard#activity') do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do - .icon-container - = navbar_icon('activity') %span Activity = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to dashboard_groups_path, title: 'Groups' do - .icon-container - = navbar_icon('group') %span Groups = nav_link(controller: 'dashboard/milestones') do = link_to dashboard_milestones_path, title: 'Milestones' do - .icon-container - = navbar_icon('milestones') %span Milestones = nav_link(path: 'dashboard#issues') do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do - .icon-container - = navbar_icon('issues') %span Issues %span.count= number_with_delimiter(current_user.assigned_issues.opened.count) = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do - .icon-container - = navbar_icon('mr') %span Merge Requests %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) = nav_link(controller: :snippets) do = link_to dashboard_snippets_path, title: 'Snippets' do - .icon-container - = icon('clipboard fw') %span Snippets = nav_link(controller: :help) do = link_to help_path, title: 'Help' do - .icon-container - = icon('question-circle fw') %span Help = nav_link(html_options: {class: profile_tab_class}) do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do - .icon-container - = icon('user fw') %span Profile Settings diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 96fe62c39c3..6d514f669db 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -18,9 +18,9 @@ %span Applications = nav_link(controller: :personal_access_tokens) do - = link_to profile_personal_access_tokens_path, title: 'Personal Access Tokens' do + = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do %span - Personal Access Tokens + Access Tokens = nav_link(controller: :emails) do = link_to profile_emails_path, title: 'Emails' do %span diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index 35c4b862bb7..ea7e3d199fd 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,4 +1,4 @@ -- if @note.diff_note? +- if @note.diff_note? && @note.diff_file %p.details New comment on diff for = link_to @note.diff_file.file_path, @target_url diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 6a067a03535..a42b3b8eb38 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -11,7 +11,7 @@ Add an SSH key %p.profile-settings-content Before you can add an SSH key you need to - = link_to "generate it.", help_page_path("ssh", "README") + = link_to "generate it.", help_page_path("ssh/README") = render 'form' %hr %h5 diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index b4d35dc9a3e..2afa026847a 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -43,12 +43,12 @@ .form-group = f.label :dashboard, class: 'label-light' do Default Dashboard - = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank') + = link_to('(?)', help_page_path('profile/preferences') + '#default-dashboard', target: '_blank') = f.select :dashboard, dashboard_choices, {}, class: 'form-control' .form-group = f.label :project_view, class: 'label-light' do Project view - = link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank') + = link_to('(?)', help_page_path('profile/preferences') + '#default-project-view', target: '_blank') = f.select :project_view, project_view_choices, {}, class: 'form-control' .help-block Choose what content you want to see on a project's home page. diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 5890456bee2..366f1fed35b 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -2,6 +2,10 @@ - header_title "Two-Factor Authentication", profile_two_factor_auth_path = render 'profiles/head' +- if inject_u2f_api? + - content_for :page_specific_javascripts do + = page_specific_javascript_tag('u2f.js') + .row.prepend-top-default .col-lg-3 %h4.prepend-top-0 @@ -14,7 +18,7 @@ - else %p Download the Google Authenticator application from App Store or Google Play Store and scan this code. - More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. + More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}. .row.append-bottom-10 .col-md-3 = raw @qr_code diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml index 2987f6b5b22..e74fd5b93ea 100644 --- a/app/views/projects/_bitbucket_import_modal.html.haml +++ b/app/views/projects/_bitbucket_import_modal.html.haml @@ -10,4 +10,4 @@ as administrator you need to configure - else ask your GitLab administrator to configure - == #{link_to 'OAuth integration', help_page_path("integration", "bitbucket")}. + == #{link_to 'OAuth integration', help_page_path("integration/bitbucket")}. diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml index 0568c2d305e..fff30f11d82 100644 --- a/app/views/projects/_builds_settings.html.haml +++ b/app/views/projects/_builds_settings.html.haml @@ -4,7 +4,7 @@ - unless @repository.gitlab_ci_yml .form-group %p Builds need to be configured before you can begin using Continuous Integration. - = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' + = link_to 'Get started with Builds', help_page_path('ci/quick_start/README'), class: 'btn btn-info' .form-group %p Get recent application code using the following command: .radio diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml index 377cf0187b8..e9f39b16aa7 100644 --- a/app/views/projects/_gitlab_import_modal.html.haml +++ b/app/views/projects/_gitlab_import_modal.html.haml @@ -10,4 +10,4 @@ as administrator you need to configure - else ask your GitLab administrator to configure - == #{link_to 'OAuth integration', help_page_path("integration", "gitlab")}. + == #{link_to 'OAuth integration', help_page_path("integration/gitlab")}. diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index 771a2e0df7d..19b4249374b 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -8,4 +8,4 @@ %strong Only allow merge requests to be merged if the build succeeds .help-block Builds need to be configured to enable this feature. - = link_to icon('question-circle'), help_page_path('workflow', 'merge_requests', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') + = link_to icon('question-circle'), help_page_path('workflow/merge_requests', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index a131289ee97..2af625f69cd 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -11,17 +11,22 @@ %span.badge.js-totalbuilds-count = number_with_delimiter(@all_builds.count(:id)) + %li{class: ('active' if @scope == 'pending')} + = link_to project_builds_path(@project, scope: :pending) do + Pending + %span.badge + = number_with_delimiter(@all_builds.pending.count(:id)) %li{class: ('active' if @scope == 'running')} = link_to project_builds_path(@project, scope: :running) do Running - %span.badge.js-running-count - = number_with_delimiter(@all_builds.running_or_pending.count(:id)) + %span.badge + = number_with_delimiter(@all_builds.running.count(:id)) %li{class: ('active' if @scope == 'finished')} = link_to project_builds_path(@project, scope: :finished) do Finished - %span.badge.js-running-count + %span.badge = number_with_delimiter(@all_builds.finished.count(:id)) .nav-controls @@ -31,12 +36,12 @@ data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - unless @repository.gitlab_ci_yml - = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' + = link_to 'Get started with Builds', help_page_path('ci/quick_start/README'), class: 'btn btn-info' = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint - %ul.content-list + %ul.content-list.builds-content-list - if @builds.blank? %li .nothing-here-block No builds to show @@ -46,14 +51,10 @@ %thead %tr %th Status - %th Build ID %th Commit - %th Ref %th Stage %th Name - %th Tags - %th Duration - %th Finished at + %th - if @project.build_coverage_enabled? %th Coverage %th diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 4e801cc72fe..4421f3b9562 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -67,4 +67,4 @@ = render "sidebar" :javascript - new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}") + new Build("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}") diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 5bd6e3f0ebc..e1b42b2cfa5 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -1,32 +1,45 @@ -%tr.build +%tr.build.commit %td.status - if can?(current_user, :read_build, build) = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build)) - else = ci_status_with_icon(build.status) - %td.build-link - - if can?(current_user, :read_build, build) - = link_to namespace_project_build_url(build.project.namespace, build.project, build) do - %strong ##{build.id} - - else - %strong ##{build.id} + %td + .branch-commit + - if can?(current_user, :read_build, build) + = link_to namespace_project_build_url(build.project.namespace, build.project, build) do + %span ##{build.id} + - else + %span ##{build.id} - - if build.stuck? - = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') - - if defined?(retried) && retried - = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') + - if build.stuck? + = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + - if defined?(retried) && retried + = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') - - if defined?(commit_sha) && commit_sha - %td - = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace" + - if defined?(ref) && ref + - if build.ref + = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" + - else + .light none + = custom_icon("icon_commit") + + - if defined?(commit_sha) && commit_sha + = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" + + .label-container + - if build.tags.any? + - build.tags.each do |tag| + %span.label.label-primary + = tag + - if build.try(:trigger_request) + %span.label.label-info triggered + - if build.try(:allow_failure) + %span.label.label-danger allowed to fail + - if defined?(retried) && retried + %span.label.label-warning retried - - if defined?(ref) && ref - %td - - if build.ref - = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref) - - else - .light none - if defined?(runner) && runner %td @@ -43,25 +56,14 @@ = build.name %td - .label-container - - if build.tags.any? - - build.tags.each do |tag| - %span.label.label-primary - = tag - - if build.try(:trigger_request) - %span.label.label-info triggered - - if build.try(:allow_failure) - %span.label.label-danger allowed to fail - - if defined?(retried) && retried - %span.label.label-warning retried - - %td.duration - if build.duration - #{duration_in_words(build.finished_at, build.started_at)} - - %td.timestamp + %p.duration + = custom_icon("icon_timer") + = duration_in_numbers(build.finished_at, build.started_at) - if build.finished_at - %span #{time_ago_with_tooltip(build.finished_at)} + %p.finished-at + = icon("calendar") + %span #{time_ago_with_tooltip(build.finished_at)} - if defined?(coverage) && coverage %td.coverage @@ -79,4 +81,4 @@ = icon('remove', class: 'cred') - elsif defined?(allow_retry) && allow_retry && build.retryable? = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do - = icon('refresh') + = icon('repeat') diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index af8dd5cd02c..0557d384e33 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -1,17 +1,18 @@ - status = pipeline.status %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: "ci-status ci-#{status}" do - = ci_icon_for_status(status) - %strong ##{pipeline.id} + = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do + = ci_status_with_icon(status) + %td - %div.branch-commit + .branch-commit + = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do + %span ##{pipeline.id} - if pipeline.ref - = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace" - · + = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name" + = custom_icon("icon_commit") = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace" - - if pipeline.tag? %span.label.label-primary tag - elsif pipeline.latest? @@ -25,12 +26,13 @@ %p.commit-title - if commit = pipeline.commit + = commit_author_avatar(commit, size: 20) = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message" - else Cant find HEAD commit for this branch - - stages_status = pipeline.statuses.stages_status + - stages_status = pipeline.statuses.latest.stages_status - stages.each do |stage| %td - status = stages_status[stage] @@ -45,27 +47,34 @@ %td - if pipeline.started_at && pipeline.finished_at %p.duration + = custom_icon("icon_timer") = duration_in_numbers(pipeline.finished_at, pipeline.started_at) + - if pipeline.finished_at + %p.finished-at + = icon("calendar") + #{time_ago_with_tooltip(pipeline.finished_at)} - %td + %td.pipeline-actions .controls.hidden-xs.pull-right - artifacts = pipeline.builds.latest.select { |b| b.artifacts? } - if artifacts.present? - .dropdown.inline.build-artifacts - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - = icon('download') - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - - artifacts.each do |build| - %li - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do - = icon("download") - %span Download '#{build.name}' artifacts + .inline + .btn-group + %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'} + = icon("download") + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + - artifacts.each do |build| + %li + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do + = icon("download") + %span Download '#{build.name}' artifacts - if can?(current_user, :update_pipeline, @project) - - if pipeline.retryable? - = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do - = icon("repeat") - - if pipeline.cancelable? - = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do - = icon("remove") + .cancel-retry-btns.inline + - if pipeline.retryable? + = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do + = icon("repeat") + - if pipeline.cancelable? + = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do + = icon("remove") diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 0411137b7c6..41fd5459429 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -42,9 +42,7 @@ %th Status %th Build ID %th Name - %th Tags - %th Duration - %th Finished at + %th - if pipeline.project.build_coverage_enabled? %th Coverage %th diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 929496f81d8..c8c7b858baa 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -25,7 +25,7 @@ .commit-actions.hidden-xs - if commit.status = render_commit_status(commit, cssclass: 'btn btn-transparent') - = clipboard_button_with_class({ clipboard_text: commit.id }, css_class: 'btn-transparent') + = clipboard_button(clipboard_text: commit.id) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" = link_to_browse_code(project, commit) diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index dd590a4b8ec..af09b3418ea 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -2,15 +2,17 @@ .clearfix - 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'} - .form-group + .form-group.dropdown.compare-form-group.js-compare-from-dropdown .input-group.inline-input-group %span.input-group-addon from - = text_field_tag :from, params[:from], class: "form-control", required: true + = text_field_tag :from, params[:from], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from].presence } + = render "ref_dropdown" = "..." - .form-group + .form-group.dropdown.compare-form-group.js-compare-to-dropdown .input-group.inline-input-group %span.input-group-addon to - = text_field_tag :to, params[:to], class: "form-control", required: true + = text_field_tag :to, params[:to], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to].presence } + = render "ref_dropdown" = button_tag "Compare", class: "btn btn-create commits-compare-btn" - if @merge_request.present? @@ -19,11 +21,3 @@ = link_to create_mr_path, class: 'prepend-left-10 btn' do = icon("plus") Create Merge Request - -:javascript - var availableTags = #{@project.repository.ref_names.to_json}; - - $("#from, #to").autocomplete({ - source: availableTags, - minLength: 1 - }); diff --git a/app/views/projects/compare/_ref_dropdown.html.haml b/app/views/projects/compare/_ref_dropdown.html.haml new file mode 100644 index 00000000000..c604c6d0135 --- /dev/null +++ b/app/views/projects/compare/_ref_dropdown.html.haml @@ -0,0 +1,4 @@ +.dropdown-menu.dropdown-menu-selectable + = dropdown_title "Select branch/tag" + = dropdown_content + = dropdown_loading diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index 894c36a96df..901605f7ca3 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -9,5 +9,5 @@ .form-group %p.light.append-bottom-0 Paste a machine public key here. Read more about how to generate it - = link_to "here", help_page_path("ssh", "README") + = link_to "here", help_page_path("ssh/README") = f.submit "Add key", class: "btn-create btn" diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml new file mode 100644 index 00000000000..0c0424edffd --- /dev/null +++ b/app/views/projects/diffs/_content.html.haml @@ -0,0 +1,29 @@ +.diff-content.diff-wrap-lines + - # Skip all non non-supported blobs + - return unless blob.respond_to?(:text?) + - if diff_file.too_large? + .nothing-here-block This diff could not be displayed because it is too large. + - elsif blob.only_display_raw? + .nothing-here-block This file is too large to display. + - elsif blob_text_viewable?(blob) + - if !project.repository.diffable?(blob) + .nothing-here-block This diff was suppressed by a .gitattributes entry. + - elsif diff_file.diff_lines.length > 0 + - if diff_file.collapsed_by_default? && !expand_all_diffs? + - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path)) + .nothing-here-block.diff-collapsed{data: { diff_for_path: url } } + This diff is collapsed. Click to expand it. + - elsif diff_view == 'parallel' + = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob + - else + = render "projects/diffs/text_file", diff_file: diff_file + - else + - if diff_file.mode_changed? + .nothing-here-block File mode changed + - elsif diff_file.renamed_file + .nothing-here-block File moved + - elsif blob.image? + - old_blob = diff_file.old_blob(diff_commit) + = render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob + - else + .nothing-here-block No preview for this file type diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 1975287faee..20aaab5accf 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -6,6 +6,8 @@ .content-block.oneline-block.files-changed .inline-parallel-buttons + - unless expand_all_diffs? + = link_to 'Expand all', url_for(params.merge(expand_all_diffs: 1, format: 'html')), class: 'btn btn-default' - if show_whitespace_toggle - if current_controller?(:commit) = commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs') @@ -21,7 +23,7 @@ - if diff_files.overflow? = render 'projects/diffs/warning', diff_files: diff_files -.files +.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, @project))}} - diff_files.each_with_index do |diff_file, index| - diff_commit = commit_for_diff(diff_file) - blob = diff_file.blob(diff_commit) diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 3b758a1ec4e..c306909fb1a 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -16,28 +16,4 @@ = view_file_btn(diff_commit.id, diff_file, project) - .diff-content.diff-wrap-lines - - # Skip all non non-supported blobs - - return unless blob.respond_to?(:text?) - - if diff_file.too_large? - .nothing-here-block This diff could not be displayed because it is too large. - - elsif blob.only_display_raw? - .nothing-here-block This file is too large to display. - - elsif blob_text_viewable?(blob) - - if !project.repository.diffable?(blob) - .nothing-here-block This diff was suppressed by a .gitattributes entry. - - elsif diff_file.diff_lines.length > 0 - - if diff_view == 'parallel' - = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - - else - = render "projects/diffs/text_file", diff_file: diff_file, index: i - - else - - if diff_file.mode_changed? - .nothing-here-block File mode changed - - elsif diff_file.renamed_file - .nothing-here-block File moved - - elsif blob.image? - - old_blob = diff_file.old_blob(diff_commit) - = render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob, index: i - - else - .nothing-here-block No preview for this file type + = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, diff_refs: diff_refs, blob: blob, project: project diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 22cad00240a..5a8a131d10c 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -13,18 +13,15 @@ %td.line_content.match= line.text - else %td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } } - - link_text = type == "new" ? " ".html_safe : line.old_pos + - link_text = type == "new" ? " " : line.old_pos - if plain = link_text - else - = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - - - if !plain && !@diff_notes_disabled && can?(current_user, :create_note, @project) - = link_to_new_diff_note(line_code, position) + %a{href: "##{line_code}", data: { linenumber: link_text }} %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - - link_text = type == "old" ? " ".html_safe : line.new_pos + - link_text = type == "old" ? " " : line.new_pos - if plain = link_text - else - = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - %td.line_content.noteable_line{ class: [type, line_code], data: { line_code: line_code, position: position.to_json } }= diff_line_content(line.text, type) + %a{href: "##{line_code}", data: { linenumber: link_text }} + %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, position, type) unless plain) }= diff_line_content(line.text, type) diff --git a/app/views/projects/diffs/_match_line_parallel.html.haml b/app/views/projects/diffs/_match_line_parallel.html.haml index 0cd888876e0..b9c0d9dcdfd 100644 --- a/app/views/projects/diffs/_match_line_parallel.html.haml +++ b/app/views/projects/diffs/_match_line_parallel.html.haml @@ -1,4 +1,4 @@ -%td.old_line.diff-line-num +%td.old_line.diff-line-num.empty-cell %td.line_content.parallel.match= line -%td.new_line.diff-line-num +%td.new_line.diff-line-num.empty-cell %td.line_content.parallel.match= line diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 51f207dce94..d208fcee10b 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,39 +1,34 @@ / Side-by-side diff view -%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight +%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ data: diff_view_data } %table - diff_file.parallel_diff_lines.each do |line| - left = line[:left] - right = line[:right] %tr.line_holder.parallel - if left[:type] == 'match' - = render "projects/diffs/match_line_parallel", { line: left[:text], - line_old: left[:number], line_new: right[:number] } + = render "projects/diffs/match_line_parallel", { line: left[:text] } - elsif left[:type] == 'nonewline' - %td.old_line.diff-line-num + %td.old_line.diff-line-num.empty-cell %td.line_content.parallel.match= left[:text] - %td.new_line.diff-line-num + %td.new_line.diff-line-num.empty-cell %td.line_content.parallel.match= left[:text] - else - %td.old_line.diff-line-num{id: left[:line_code], class: [left[:type], ('empty-cell' unless left[:number])]} - = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - - if !@diff_notes_disabled && can?(current_user, :create_note, @project) - = link_to_new_diff_note(left[:line_code], left[:position], 'old') - %td.line_content.parallel.noteable_line{class: [left[:type], left[:line_code], ('empty-cell' if left[:text].empty?)], data: { line_code: left[:line_code], position: left[:position].to_json }}= diff_line_content(left[:text]) + %td.old_line.diff-line-num{id: left[:line_code], class: [left[:type], ('empty-cell' unless left[:number])], data: { linenumber: left[:number] }} + %a{href: "##{left[:line_code]}" }= raw(left[:number]) + %td.line_content.parallel.noteable_line{class: [left[:type], ('empty-cell' if left[:text].empty?)], data: diff_view_line_data(left[:line_code], left[:position], 'old')}= diff_line_content(left[:text]) - if right[:type] == 'new' - - new_line_class = 'new' + - new_line_type = 'new' - new_line_code = right[:line_code] - new_position = right[:position] - else - - new_line_class = nil + - new_line_type = nil - new_line_code = left[:line_code] - new_position = left[:position] - %td.new_line.diff-line-num{id: new_line_code, class: [new_line_class, ('empty-cell' unless right[:number])], data: { linenumber: right[:number] }} - = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - - if !@diff_notes_disabled && can?(current_user, :create_note, @project) - = link_to_new_diff_note(new_line_code, new_position, 'new') - %td.line_content.parallel.noteable_line{class: [new_line_class, new_line_code, ('empty-cell' if right[:text].empty?)], data: { line_code: new_line_code, position: new_position.to_json }}= diff_line_content(right[:text]) + %td.new_line.diff-line-num{id: new_line_code, class: [new_line_type, ('empty-cell' unless right[:number])], data: { linenumber: right[:number] }} + %a{href: "##{new_line_code}" }= raw(right[:number]) + %td.line_content.parallel.noteable_line{class: [new_line_type, ('empty-cell' if right[:text].empty?)], data: diff_view_line_data(new_line_code, new_position, 'new')}= diff_line_content(right[:text]) - unless @diff_notes_disabled - notes_left, notes_right = organize_comments(left, right) diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 192093d1273..196f8122db3 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -3,7 +3,7 @@ .suppressed-container %a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show. -%table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' } +%table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' } - last_line = 0 - diff_file.highlighted_diff_lines.each do |line| - last_line = line.new_pos diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index 15536c17f8e..10fa1ddf2e5 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -2,9 +2,6 @@ %h4 Too many changes to show. .pull-right - - unless diff_hard_limit_enabled? - = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: nil)), class: "btn btn-sm" - - if current_controller?(:commit) or current_controller?(:merge_requests) - if current_controller?(:commit) = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-sm" diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 27a94fe02dc..57af167180b 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -23,7 +23,7 @@ .form-group.project-visibility-level-holder = f.label :visibility_level, class: 'label-light' do Visibility Level - = link_to "(?)", help_page_path("public_access", "public_access") + = link_to "(?)", help_page_path("public_access/public_access") - if can_change_visibility_level?(@project, current_user) = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project) - else diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 5242021243e..303d7c23d01 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -17,7 +17,7 @@ Environments are places where code gets deployed, such as staging or production. %br = succeed "." do - = link_to "Read more about environments", help_page_path("ci", "environments") + = link_to "Read more about environments", help_page_path("ci/environments") - if can?(current_user, :create_environment, @project) = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml index da325efecd2..89e06567196 100644 --- a/app/views/projects/environments/new.html.haml +++ b/app/views/projects/environments/new.html.haml @@ -7,6 +7,6 @@ %p Environments allow you to track deployments of your application = succeed "." do - = link_to "Read more about environments", help_page_path("ci", "environments") + = link_to "Read more about environments", help_page_path("ci/environments") = render 'form' diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 53c62ef234d..b17aba2431f 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -20,7 +20,7 @@ Define environments in the deploy stage(s) in %code .gitlab-ci.yml to track deployments here. - = link_to "Read more", help_page_path("ci", "environments"), class: "btn btn-success" + = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" - else .table-holder %table.table.environments diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 5bc5c71283e..542827b2f15 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -50,10 +50,12 @@ %td.duration - if generic_commit_status.duration + = icon("clock-o") #{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)} %td.timestamp - if generic_commit_status.finished_at + = icon("calendar") %span #{time_ago_with_tooltip(generic_commit_status.finished_at)} - if defined?(coverage) && coverage diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 312bd86ed04..7612fe3719a 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -32,7 +32,7 @@ Code, test, and deploy together .blank-state .blank-state-icon - = navbar_icon("issues", size: 50) + = custom_icon("issues", size: 50) %h3.blank-state-title You don't have any issues right now. %p.blank-state-text 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 b3bea900d42..b727efaa6a6 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 @@ -8,7 +8,7 @@ %p %strong Step 1. Fetch and check out the branch for this merge request - = clipboard_button_with_class({clipboard_target: "pre#merge-info-1"}, css_class: "btn-clipboard") + = clipboard_button(clipboard_target: "pre#merge-info-1") %pre.dark#merge-info-1 - if @merge_request.for_fork? :preserve @@ -25,7 +25,7 @@ %p %strong Step 3. Merge the branch and fix any conflicts that come up - = clipboard_button_with_class({clipboard_target: "pre#merge-info-3"}, css_class: "btn-clipboard") + = clipboard_button(clipboard_target: "pre#merge-info-3") %pre.dark#merge-info-3 - if @merge_request.for_fork? :preserve @@ -38,7 +38,7 @@ %p %strong Step 4. Push the result of the merge to GitLab - = clipboard_button_with_class({clipboard_target: "pre#merge-info-4"}, css_class: "btn-clipboard") + = clipboard_button(clipboard_target: "pre#merge-info-4") %pre.dark#merge-info-4 :preserve git push origin #{h @merge_request.target_branch} 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 5bf5210aeab..b24bdf22ceb 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -19,13 +19,13 @@ Options .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul - %li{ class: issue_button_visibility(@merge_request, true) } + %li{ class: merge_request_button_visibility(@merge_request, true) } = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' - %li{ class: issue_button_visibility(@merge_request, false) } + %li{ class: merge_request_button_visibility(@merge_request, false) } = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' %li = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit' - = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request' - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' + = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{merge_request_button_visibility(@merge_request, true)}", title: 'Close merge request' + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{merge_request_button_visibility(@merge_request, false)}", title: 'Reopen merge request' = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" do Edit diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 05f33b78a47..c72d0140bb9 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -107,7 +107,7 @@ .form-group.project-visibility-level-holder = f.label :visibility_level, class: 'label-light' do Visibility Level - = link_to "(?)", help_page_path("public_access", "public_access") + = link_to "(?)", help_page_path("public_access/public_access") = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project) = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 @@ -154,4 +154,9 @@ $('.import_gitlab_project').attr('disabled',true); $('.import_gitlab_project').attr('title', 'Project path required.'); } - }) + }); + + $('.import_git').click(function( event ) { + $projectImportUrl = $('#project_import_url') + $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled')) + });
\ No newline at end of file diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml index 7d1cbc62e86..25466e7562e 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/projects/notes/_hints.html.haml @@ -1,7 +1,7 @@ .comment-toolbar.clearfix .toolbar-text Styling with - = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 + = link_to 'Markdown', help_page_path('markdown/markdown'), target: '_blank', tabindex: -1 is supported %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' } = icon('file-image-o', class: 'toolbar-button-icon') diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 1c39ce897a3..56d302fab82 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -2,6 +2,8 @@ = render "projects/notes/notes" %ul.notes.notes-form.timeline %li.timeline-entry + .flash-container.timeline-content + - if can? current_user, :create_note, @project .timeline-icon.hidden-xs.hidden-sm %a.author_link{ href: user_path(current_user) } diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 6a127afa410..5f466bdbac2 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -28,10 +28,10 @@ .nav-controls - if can? current_user, :create_pipeline, @project = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do - New pipeline + Run pipeline - unless @repository.gitlab_ci_yml - = link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' + = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint @@ -45,13 +45,13 @@ .table-holder %table.table.builds %tbody - %th ID + %th Status %th Commit - stages.each do |stage| %th.stage %span.has-tooltip{ title: "#{stage.titleize}" } = stage.titleize - %th Duration + %th %th = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 82892a33358..978c4dfc5ec 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -12,7 +12,7 @@ = select_tag :access_level, options_for_select(ProjectMember.access_level_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" + %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" .form-actions = f.submit 'Add users to project', class: "btn btn-create" diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 5669713d9a1..883d3e3af1e 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -8,10 +8,10 @@ %p.prepend-top-20 Protected branches are designed to: %ul - %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} + %li prevent pushes from everybody except #{link_to "masters", help_page_path("user/permissions"), class: "vlink"} %li prevent anyone from force pushing to the branch %li prevent anyone from deleting the branch - %p.append-bottom-0 Read more about #{link_to "project permissions", help_page_path("permissions", "permissions"), class: "underlined-link"} + %p.append-bottom-0 Read more about #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"} .col-lg-9 %h5.prepend-top-0 Protect a branch @@ -23,7 +23,7 @@ = f.label :name, "Branch", class: "label-light" = render partial: "dropdown", locals: { f: f } %p.help-block - = link_to "Wildcards", help_page_path(category: 'workflow', file: 'protected_branches', format: 'md', anchor: "wildcard-protected-branches") + = link_to "Wildcards", help_page_path('workflow/protected_branches', anchor: "wildcard-protected-branches") such as %code *-stable or diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 58d8e068754..dd1cf680cfa 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -82,4 +82,4 @@ Archived project! Repository is read-only %div{class: "project-show-#{default_project_view}"} - = render default_project_view + = render default_project_view
\ No newline at end of file diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 6c994ae486b..1646bcf4b8a 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,6 +1,6 @@ - page_title "Snippets" -.row-content-block.top-block +.sub-header-block .pull-right - if can?(current_user, :create_project_snippet, @project) = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 627814bcfae..65a3a6bddab 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -2,7 +2,7 @@ = f.label :import_url, class: 'control-label' do %span Git repository URL .col-sm-10 - = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' + = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true .well.prepend-top-20 %ul diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index 1c6ec198d3d..107ad19177c 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -1,7 +1,7 @@ .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") + = 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) diff --git a/app/views/shared/icons/_group.svg b/app/views/shared/icons/_group.svg.erb index 75cae0d16c8..53635016900 100644 --- a/app/views/shared/icons/_group.svg +++ b/app/views/shared/icons/_group.svg.erb @@ -1,9 +1,4 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> - <title>Group</title> - <desc>Created with Sketch.</desc> - <defs></defs> +<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16"> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Group" fill="#303030"> <path d="M15.6667,10.0105 L10.3337,10.0105 C10.1497,10.0105 9.9997,10.1775 9.9997,10.3845 L9.9997,15.6145 C9.9997,15.8215 10.1497,15.9885 10.3337,15.9885 L15.6667,15.9885 C15.8507,15.9885 15.9997,15.8215 15.9997,15.6145 L15.9997,10.3845 C15.9997,10.1775 15.8507,10.0105 15.6667,10.0105 L15.6667,10.0105 L15.6667,10.0105 Z M11.9997,14.0105 L13.9997,14.0105 L13.9997,12.0105 L11.9997,12.0105 L11.9997,14.0105 L11.9997,14.0105 Z" id="Fill-11"></path> @@ -15,4 +10,4 @@ <path d="M11.6667,6.21724894e-15 L4.3337,6.21724894e-15 C4.1497,6.21724894e-15 3.9997,0.167 3.9997,0.374 L3.9997,6.604 C3.9997,6.811 4.1497,6.978 4.3337,6.978 L11.6667,6.978 C11.8507,6.978 11.9997,6.811 11.9997,6.604 L11.9997,0.374 C11.9997,0.167 11.8507,6.21724894e-15 11.6667,6.21724894e-15 L11.6667,6.21724894e-15 L11.6667,6.21724894e-15 Z M5.9997,5 L9.9997,5 L9.9997,2 L5.9997,2 L5.9997,5 L5.9997,5 Z" id="Fill-14"></path> </g> </g> -</svg>
\ No newline at end of file +</svg> diff --git a/app/views/shared/icons/_icon_commit.svg b/app/views/shared/icons/_icon_commit.svg new file mode 100644 index 00000000000..0e96035b7b7 --- /dev/null +++ b/app/views/shared/icons/_icon_commit.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"> + <path fill="#8F8F8F" fill-rule="evenodd" d="M28.7769836,18 C27.8675252,13.9920226 24.2831748,11 20,11 C15.7168252,11 12.1324748,13.9920226 11.2230164,18 L4.0085302,18 C2.90195036,18 2,18.8954305 2,20 C2,21.1122704 2.8992496,22 4.0085302,22 L11.2230164,22 C12.1324748,26.0079774 15.7168252,29 20,29 C24.2831748,29 27.8675252,26.0079774 28.7769836,22 L35.9914698,22 C37.0980496,22 38,21.1045695 38,20 C38,18.8877296 37.1007504,18 35.9914698,18 L28.7769836,18 L28.7769836,18 Z M20,25 C22.7614237,25 25,22.7614237 25,20 C25,17.2385763 22.7614237,15 20,15 C17.2385763,15 15,17.2385763 15,20 C15,22.7614237 17.2385763,25 20,25 L20,25 Z"/> +</svg> diff --git a/app/views/shared/icons/_icon_timer.svg b/app/views/shared/icons/_icon_timer.svg new file mode 100644 index 00000000000..0b1e5804427 --- /dev/null +++ b/app/views/shared/icons/_icon_timer.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><g fill="#8F8F8F" fill-rule="evenodd"><path d="M29.513 10.134A15.922 15.922 0 0 0 23 7.28V6h2.993C26.55 6 27 5.552 27 5V2a1 1 0 0 0-1.007-1H14.007C13.45 1 13 1.448 13 2v3a1 1 0 0 0 1.007 1H17v1.28C9.597 8.686 4 15.19 4 23c0 8.837 7.163 16 16 16s16-7.163 16-16c0-3.461-1.099-6.665-2.967-9.283l1.327-1.58a2.498 2.498 0 0 0-.303-3.53 2.499 2.499 0 0 0-3.528.315l-1.016 1.212zM20 34c6.075 0 11-4.925 11-11s-4.925-11-11-11S9 16.925 9 23s4.925 11 11 11z"/><path d="M19 21h-4.002c-.552 0-.998.452-.998 1.01v1.98c0 .567.447 1.01.998 1.01h7.004c.274 0 .521-.111.701-.291a.979.979 0 0 0 .297-.704v-8.01c0-.54-.452-.995-1.01-.995h-1.98a.997.997 0 0 0-1.01.995V21z"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_project.svg b/app/views/shared/icons/_project.svg deleted file mode 100644 index 1e8b43f8c6b..00000000000 --- a/app/views/shared/icons/_project.svg +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> - <title>Page 1</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <path d="M6,6 L12,6 L12,5 L6,5 L6,6 Z M6,8 L12,8 L12,7 L6,7 L6,8 Z M6,10 L12,10 L12,9 L6,9 L6,10 Z M6,12 L12,12 L12,11 L6,11 L6,12 Z M4,6 L5,6 L5,5 L4,5 L4,6 Z M4,8 L5,8 L5,7 L4,7 L4,8 Z M4,10 L5,10 L5,9 L4,9 L4,10 Z M4,12 L5,12 L5,11 L4,11 L4,12 Z M13,3 L10,3 L10,4 L6,4 L6,3 L3,3 L3,13 L13,13 L13,3 Z M2,14 L14,14 L14,2 L2,2 L2,14 Z M1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 L1,0 Z" fill="#7F7E7E"></path> - </g> -</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_project.svg.erb b/app/views/shared/icons/_project.svg.erb new file mode 100644 index 00000000000..2f60bb7245e --- /dev/null +++ b/app/views/shared/icons/_project.svg.erb @@ -0,0 +1,3 @@ +<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16"> + <path d="M6,6 L12,6 L12,5 L6,5 L6,6 Z M6,8 L12,8 L12,7 L6,7 L6,8 Z M6,10 L12,10 L12,9 L6,9 L6,10 Z M6,12 L12,12 L12,11 L6,11 L6,12 Z M4,6 L5,6 L5,5 L4,5 L4,6 Z M4,8 L5,8 L5,7 L4,7 L4,8 Z M4,10 L5,10 L5,9 L4,9 L4,10 Z M4,12 L5,12 L5,11 L4,11 L4,12 Z M13,3 L10,3 L10,4 L6,4 L6,3 L3,3 L3,13 L13,13 L13,3 Z M2,14 L14,14 L14,2 L2,2 L2,14 Z M1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 L1,0 Z" fill="#7F7E7E" fill-rule="evenodd"></path> +</svg> diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index adfab1af53e..e020a7d4d00 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -19,7 +19,7 @@ = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| .block.assignee - .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.to_reference if issuable.assignee)} + .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.name if issuable.assignee)} - if issuable.assignee = link_to_member(@project, issuable.assignee, size: 24) - else diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml index 975c74f4ea6..dee2472fa79 100644 --- a/app/views/shared/milestones/_summary.html.haml +++ b/app/views/shared/milestones/_summary.html.haml @@ -26,7 +26,6 @@ %span.pull-right.tab-issues-buttons - if project && can?(current_user, :create_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 = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped" %span.pull-right.tab-merge-requests-buttons.hidden diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index d1e861ca80c..2585ed9360b 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -6,7 +6,7 @@ %h4.prepend-top-0 = page_title %p - #{link_to "Webhooks", help_page_path("web_hooks", "web_hooks")} can be + #{link_to "Webhooks", help_page_path("web_hooks/web_hooks")} can be used for binding events when something is happening within the project. .col-lg-9.append-bottom-default = form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f| diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb new file mode 100644 index 00000000000..a6cefd4d601 --- /dev/null +++ b/app/workers/git_garbage_collect_worker.rb @@ -0,0 +1,16 @@ +class GitGarbageCollectWorker + include Sidekiq::Worker + include Gitlab::ShellAdapter + + sidekiq_options queue: :gitlab_shell, retry: false + + def perform(project_id) + project = Project.find(project_id) + + gitlab_shell.gc(project.repository_storage_path, project.path_with_namespace) + # Refresh the branch cache in case garbage collection caused a ref lookup to fail + project.repository.after_create_branch + project.repository.branch_names + project.repository.has_visible_content? + end +end diff --git a/app/workers/gitlab_shell_one_shot_worker.rb b/app/workers/gitlab_shell_one_shot_worker.rb deleted file mode 100644 index 4ddbcf574d5..00000000000 --- a/app/workers/gitlab_shell_one_shot_worker.rb +++ /dev/null @@ -1,10 +0,0 @@ -class GitlabShellOneShotWorker - include Sidekiq::Worker - include Gitlab::ShellAdapter - - sidekiq_options queue: :gitlab_shell, retry: false - - def perform(action, *arg) - gitlab_shell.send(action, *arg) - end -end diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb index 39f6037e077..615311e63f5 100644 --- a/app/workers/project_export_worker.rb +++ b/app/workers/project_export_worker.rb @@ -1,7 +1,7 @@ class ProjectExportWorker include Sidekiq::Worker - sidekiq_options queue: :gitlab_shell, retry: true + sidekiq_options queue: :gitlab_shell, retry: 3 def perform(current_user_id, project_id) current_user = User.find(current_user_id) diff --git a/config/application.rb b/config/application.rb index 21e7cc7b6e8..5f7b6a3c049 100644 --- a/config/application.rb +++ b/config/application.rb @@ -87,6 +87,7 @@ module Gitlab config.assets.precompile << "profile/application.js" config.assets.precompile << "lib/utils/*.js" config.assets.precompile << "lib/*.js" + config.assets.precompile << "u2f.js" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/config/gitlab.teatro.yml b/config/gitlab.teatro.yml deleted file mode 100644 index 75b79b837e0..00000000000 --- a/config/gitlab.teatro.yml +++ /dev/null @@ -1,87 +0,0 @@ - -production: &base - gitlab: - host: localhost - port: 80 - https: false - - user: root - - email_from: example@example.com - - support_email: support@example.com - - default_projects_features: - issues: true - merge_requests: true - wiki: true - snippets: false - visibility_level: "private" # can be "private" | "internal" | "public" - - issues_tracker: - - gravatar: - enabled: true # Use user avatar image from Gravatar.com (default: true) - - ldap: - enabled: false - host: '_your_ldap_server' - port: 636 - uid: 'sAMAccountName' - method: 'ssl' # "tls" or "ssl" or "plain" - bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' - password: '_the_password_of_the_bind_user' - allow_username_or_email_login: true - - base: '' - - user_filter: '' - - omniauth: - enabled: false - - satellites: - # Relative paths are relative to Rails.root (default: tmp/repo_satellites/) - path: /apps/gitlab-satellites/ - - backup: - path: "tmp/backups" # Relative paths are relative to Rails.root (default: tmp/backups/) - - repositories: - storages: # REPO PATHS MUST NOT BE A SYMLINK!!! - default: /apps/repositories/ - - gitlab_shell: - path: /apps/gitlab-shell/ - - hooks_path: /apps/gitlab-shell/hooks/ - - upload_pack: true - receive_pack: true - - git: - bin_path: /usr/bin/git - max_size: 5242880 # 5.megabytes - timeout: 10 - - extra: - -development: - <<: *base - -test: - <<: *base - gravatar: - enabled: true - gitlab: - host: localhost - port: 80 - issues_tracker: - redmine: - title: "Redmine" - project_url: "http://redmine/projects/:issues_tracker_id" - issues_url: "http://redmine/:project_id/:issues_tracker_id/:id" - new_issue_url: "http://redmine/projects/:issues_tracker_id/issues/new" - -staging: - <<: *base diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb index 6796407d4e6..4c91a61fb4a 100644 --- a/config/initializers/health_check.rb +++ b/config/initializers/health_check.rb @@ -1,16 +1,3 @@ -# Email forcibly included in the standard checks, but the email health check -# doesn't support the full range of SMTP options, which can result in failures -# for valid SMTP configurations. -# Overwrite the HealthCheck's detection of whether email is configured -# in order to avoid the email check during standard checks -module HealthCheck - class Utils - def self.mailer_configured? - false - end - end -end - HealthCheck.setup do |config| config.standard_checks = ['database', 'migrations', 'cache'] config.full_checks = ['database', 'migrations', 'cache'] diff --git a/config/routes.rb b/config/routes.rb index ea6465038df..97c1e8d2154 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -89,8 +89,9 @@ Rails.application.routes.draw do mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\/(info\/lfs|gitlab-lfs)/.match(request.path_info) }, via: [:get, :post, :put] # Help + get 'help' => 'help#index' - get 'help/:category/:file' => 'help#show', as: :help_page, constraints: { category: /.*/, file: /[^\/\.]+/ } + get 'help/*path' => 'help#show', as: :help_page get 'help/shortcuts' get 'help/ui' => 'help#ui' @@ -615,10 +616,18 @@ Rails.application.routes.draw do post :retry_builds post :revert post :cherry_pick + get :diff_for_path end end - resources :compare, only: [:index, :create] + resources :compare, only: [:index, :create] do + collection do + get :diff_for_path + end + end + + get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ } + resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do @@ -629,9 +638,6 @@ Rails.application.routes.draw do end end - get '/compare/:from...:to' => 'compare#show', :as => 'compare', - :constraints => { from: /.+/, to: /.+/ } - resources :snippets, constraints: { id: /\d+/ } do member do get 'raw' @@ -706,12 +712,14 @@ Rails.application.routes.draw do post :toggle_subscription post :toggle_award_emoji post :remove_wip + get :diff_for_path end collection do get :branch_from get :branch_to get :update_branches + get :diff_for_path end end diff --git a/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb b/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb new file mode 100644 index 00000000000..668c22bb51c --- /dev/null +++ b/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb @@ -0,0 +1,21 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveAwardEmojisWithNoUser < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def up + AwardEmoji.joins('LEFT JOIN users ON users.id = user_id').where('users.id IS NULL').destroy_all + end +end diff --git a/db/migrate/20160715132507_add_user_id_to_pipeline.rb b/db/migrate/20160715132507_add_user_id_to_pipeline.rb new file mode 100644 index 00000000000..af0461c4daf --- /dev/null +++ b/db/migrate/20160715132507_add_user_id_to_pipeline.rb @@ -0,0 +1,7 @@ +class AddUserIdToPipeline < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + def change + add_column :ci_commits, :user_id, :integer + end +end diff --git a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb new file mode 100644 index 00000000000..7c991c6d998 --- /dev/null +++ b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb @@ -0,0 +1,9 @@ +class AddIndexForPipelineUserId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def change + add_concurrent_index :ci_commits, :user_id + end +end diff --git a/db/migrate/20160716115710_add_when_and_yaml_variables_to_ci_builds.rb b/db/migrate/20160716115710_add_when_and_yaml_variables_to_ci_builds.rb new file mode 100644 index 00000000000..3e084023a65 --- /dev/null +++ b/db/migrate/20160716115710_add_when_and_yaml_variables_to_ci_builds.rb @@ -0,0 +1,8 @@ +class AddWhenAndYamlVariablesToCiBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + def change + add_column :ci_builds, :when, :string + add_column :ci_builds, :yaml_variables, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index a5eea3a697c..d250d65fe21 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160705163108) do +ActiveRecord::Schema.define(version: 20160716115710) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -168,6 +168,8 @@ ActiveRecord::Schema.define(version: 20160705163108) do t.string "environment" t.datetime "artifacts_expire_at" t.integer "artifacts_size" + t.string "when" + t.text "yaml_variables" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree @@ -199,6 +201,7 @@ ActiveRecord::Schema.define(version: 20160705163108) do t.datetime "started_at" t.datetime "finished_at" t.integer "duration" + t.integer "user_id" end add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree @@ -210,6 +213,7 @@ ActiveRecord::Schema.define(version: 20160705163108) do add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree + add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree create_table "ci_events", force: :cascade do |t| t.integer "project_id" diff --git a/doc/README.md b/doc/README.md index cf7a828d91e..cc0b6e0c1e5 100644 --- a/doc/README.md +++ b/doc/README.md @@ -11,7 +11,7 @@ - [Importing and exporting projects between instances](user/project/settings/import_export.md). - [Markdown](markdown/markdown.md) GitLab's advanced formatting system. - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab. -- [Permissions](permissions/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do. +- [Permissions](user/permissions.md) Learn what each role in a project (external/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. diff --git a/doc/api/issues.md b/doc/api/issues.md index 3ced787b23e..419fb8f85d8 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -78,7 +78,8 @@ Example response: "iid" : 6, "labels" : [], "subscribed" : false, - "user_notes_count": 1 + "user_notes_count": 1, + "due_date": "2016-07-22" } ] ``` @@ -154,7 +155,8 @@ Example response: "updated_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z", "subscribed" : false, - "user_notes_count": 1 + "user_notes_count": 1, + "due_date": null } ] ``` @@ -232,7 +234,8 @@ Example response: "updated_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z", "subscribed" : false, - "user_notes_count": 1 + "user_notes_count": 1, + "due_date": "2016-07-22" } ] ``` @@ -295,7 +298,8 @@ Example response: "updated_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z", "subscribed": false, - "user_notes_count": 1 + "user_notes_count": 1, + "due_date": null } ``` @@ -320,6 +324,7 @@ POST /projects/:id/issues | `milestone_id` | integer | no | The ID of a milestone to assign issue | | `labels` | string | no | Comma-separated label names for an issue | | `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` | +| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | ```bash curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug @@ -351,7 +356,8 @@ Example response: "updated_at" : "2016-01-07T12:44:33.959Z", "milestone" : null, "subscribed" : true, - "user_notes_count": 0 + "user_notes_count": 0, + "due_date": null } ``` @@ -379,6 +385,7 @@ PUT /projects/:id/issues/:issue_id | `labels` | string | no | Comma-separated label names for an issue | | `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | | `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` | +| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | ```bash curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close @@ -410,7 +417,8 @@ Example response: "assignee" : null, "milestone" : null, "subscribed" : true, - "user_notes_count": 0 + "user_notes_count": 0, + "due_date": "2016-07-22" } ``` @@ -487,7 +495,8 @@ Example response: "state": "active", "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", "web_url": "https://gitlab.example.com/u/solon.cremin" - } + }, + "due_date": null } ``` @@ -541,7 +550,8 @@ Example response: "state": "active", "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", "web_url": "https://gitlab.example.com/u/solon.cremin" - } + }, + "due_date": null } ``` @@ -596,7 +606,8 @@ Example response: "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", "web_url": "https://gitlab.example.com/u/orville" }, - "subscribed": false + "subscribed": false, + "due_date": null } ``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 816f09e1007..a8c3b068d22 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -68,7 +68,9 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : false, - "user_notes_count": 1 + "user_notes_count": 1, + "should_remove_source_branch": true, + "force_remove_source_branch": false } ] ``` @@ -132,7 +134,9 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, - "user_notes_count": 1 + "user_notes_count": 1, + "should_remove_source_branch": true, + "force_remove_source_branch": false } ``` @@ -233,6 +237,8 @@ Parameters: "merge_status": "can_be_merged", "subscribed" : true, "user_notes_count": 1, + "should_remove_source_branch": true, + "force_remove_source_branch": false, "changes": [ { "old_path": "VERSION", @@ -312,7 +318,9 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, - "user_notes_count": 0 + "user_notes_count": 0, + "should_remove_source_branch": true, + "force_remove_source_branch": false } ``` @@ -383,7 +391,9 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, - "user_notes_count": 1 + "user_notes_count": 1, + "should_remove_source_branch": true, + "force_remove_source_branch": false } ``` @@ -481,7 +491,9 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, - "user_notes_count": 1 + "user_notes_count": 1, + "should_remove_source_branch": true, + "force_remove_source_branch": false } ``` @@ -547,7 +559,9 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, - "user_notes_count": 1 + "user_notes_count": 1, + "should_remove_source_branch": true, + "force_remove_source_branch": false } ``` @@ -866,7 +880,9 @@ Example response: "merge_when_build_succeeds": false, "merge_status": "unchecked", "subscribed": true, - "user_notes_count": 7 + "user_notes_count": 7, + "should_remove_source_branch": true, + "force_remove_source_branch": false }, "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/merge_requests/7", "body": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.", diff --git a/doc/api/todos.md b/doc/api/todos.md index 29e73664410..23f6e35f2a4 100644 --- a/doc/api/todos.md +++ b/doc/api/todos.md @@ -15,7 +15,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, or `marked`. | +| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, `marked`, or `approval_required`. | | `author_id` | integer | no | The ID of an author | | `project_id` | integer | no | The ID of a project | | `state` | string | no | The state of the todo. Can be either `pending` or `done` | diff --git a/doc/ci/README.md b/doc/ci/README.md index a9d407528e8..0833027f91d 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -15,6 +15,6 @@ - [Use SSH keys in your build environment](ssh_keys/README.md) - [Trigger builds through the API](triggers/README.md) - [Build artifacts](build_artifacts/README.md) -- [User permissions](permissions/README.md) +- [User permissions](../user/permissions.md#gitlab-ci) - [API](../api/ci/README.md) - [CI services (linked docker containers)](services/README.md) diff --git a/doc/ci/permissions/README.md b/doc/ci/permissions/README.md index d77061c14cd..42eb59f84c8 100644 --- a/doc/ci/permissions/README.md +++ b/doc/ci/permissions/README.md @@ -1,24 +1,3 @@ # Users Permissions -GitLab CI relies on user's role on the GitLab. There are three permissions levels on GitLab CI: admin, master, developer, other. - -Admin user can perform any actions on GitLab CI in scope of instance and project. Also user with admin permission can use admin interface. - - - - -| Action | Guest, Reporter | Developer | Master | Admin | -|---------------------------------------|-----------------|-------------|----------|--------| -| See commits and builds | ✓ | ✓ | ✓ | ✓ | -| Retry or cancel build | | ✓ | ✓ | ✓ | -| Remove project | | | ✓ | ✓ | -| Create project | | | ✓ | ✓ | -| Change project configuration | | | ✓ | ✓ | -| Add specific runners | | | ✓ | ✓ | -| Add shared runners | | | | ✓ | -| See events in the system | | | | ✓ | -| Admin interface | | | | ✓ | - - - - +This document was moved to [user/permissions.md](../../user/permissions.md#gitlab-ci). diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index eb81267242e..50fa263f693 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -133,7 +133,7 @@ builds, including deploy builds. This can be an array or a multi-line string. ### after_script >**Note:** -Introduced in GitLab 8.7 and requires Gitlab Runner v1.2 (not yet released) +Introduced in GitLab 8.7 and requires Gitlab Runner v1.2 `after_script` is used to define the command that will be run after for all builds. This has to be an array or a multi-line string. @@ -985,11 +985,11 @@ directive defined in `.postgres_services` and `.mysql_services` respectively: - ruby test:postgres: - << *job_definition + <<: *job_definition services: *postgres_definition test:mysql: - << *job_definition + <<: *job_definition services: *mysql_definition ``` diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 975bb82c37d..fac35ec964d 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -44,7 +44,7 @@ it organized and easy to find. - When introducing a new document, be careful for the headings to be grammatically and syntactically correct. It is advised to mention one or all of the following GitLab members for a review: `@axil`, `@rspeicher`, - `@dblessing`, `@ashleys`, `@nearlythere`. This is to ensure that no document + `@dblessing`, `@ashleys`. This is to ensure that no document with wrong heading is going live without an audit, thus preventing dead links and redirection issues when corrected - Leave exactly one newline after a heading diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index ce0aaa2fd25..65252288019 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -1,43 +1,45 @@ -# UI Guide for building GitLab +# UI Guide for building GitLab ## 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 +When you run GitLab instance locally - just visit http://localhost:3000/help/ui page to see UI examples you can use during GitLab development. ## Design repository -All design files are stored in the [gitlab-design](https://gitlab.com/gitlab-org/gitlab-design) -repository and maintained by GitLab UX designers. +All design files are stored in the [gitlab-design](https://gitlab.com/gitlab-org/gitlab-design) +repository and maintained by GitLab UX designers. ## Navigation -GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu. -This menu will be visible regardless of what page you visit. The left sidebar also contains the GitLab logo -and the current user's profile picture. The content section contains a header and the content itself. -The header describes the current GitLab page and what navigation is -available to user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example when user visits one of the +GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu. +This menu will be visible regardless of what page you visit. The left sidebar also contains the GitLab logo +and the current user's profile picture. The content section contains a header and the content itself. +The header describes the current GitLab page and what navigation is +available to user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example when user visits one of the project pages the header will contain a project name and navigation for that project. When the user visits a group page it will contain a group name and navigation related to this group. ### Adding new tab to header navigation -We try to keep the amount of tabs in the header navigation between 5 and 10 so that it fits on a typical laptop screen. We also try not to confuse the user with too many options. Ideally each -tab should represent separate functionality. Everything related to the issue -tracker should be under the 'Issues' tab while everything related to the wiki should +We try to keep the amount of tabs in the header navigation between 5 and 10 so that it fits on a typical laptop screen. We also try not to confuse the user with too many options. Ideally each +tab should represent separate functionality. Everything related to the issue +tracker should be under the 'Issues' tab while everything related to the wiki should be under 'Wiki' tab and so on and so forth. +When adding a new tab to the header don't use more than 2 words for text in the link. +We want to keep links short and easy to remember and fit all of them in the small screen. -## Mobile screen size +## Mobile screen size -We want GitLab to work well on small mobile screens as well. Size limitations make it is impossible to fit everything on a mobile screen. In this case it is OK to hide -part of the UI for smaller resolutions in favor of a better user experience. +We want GitLab to work well on small mobile screens as well. Size limitations make it is impossible to fit everything on a mobile screen. In this case it is OK to hide +part of the UI for smaller resolutions in favor of a better user experience. However core functionality like browsing files, creating issues, writing comments, should be available on all resolutions. ## Icons -* `trash` icon for button or link that does destructive action like removing +* `trash` icon for button or link that does destructive action like removing information from database or file system * `x` icon for closing/hiding UI element. For example close modal window * `pencil` icon for edit button or link @@ -50,8 +52,14 @@ information from database or file system * Button should contain icon or text. Exceptions should be approved by UX designer. * Use red button for destructive actions (not revertable). For example removing issue. -* Use green or blue button for primary action. Primary button should be only one. -Do not use both green and blue button in one form. -* For all other cases use default white button. -* Text button should have only first word capitalized. So should be "Create issue" instead of "Create Issue" +* Use green or blue button for primary action. Primary button should be only one. +Do not use both green and blue button in one form. +* For all other cases use default white button. +* Text button should have only first word capitalized. So should be "Create issue" instead of "Create Issue" +## Counts + +* Always use the [`number_with_delimiter`][number_with_delimiter] helper to + display counts in the UI. + +[number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md index 5f8bb57365c..0c53584d201 100644 --- a/doc/integration/oauth_provider.md +++ b/doc/integration/oauth_provider.md @@ -28,7 +28,8 @@ GitLab supports two ways of adding a new OAuth2 application to an instance. You can either add an application as a regular user or add it in the admin area. What this means is that GitLab can actually have instance-wide and a user-wide applications. There is no difference between them except for the different -permission levels they are set (user/admin). +permission levels they are set (user/admin). The default callback URL is +`http://your-gitlab.example.com/users/auth/gitlab/callback` ## Adding an application through the profile diff --git a/doc/integration/saml.md b/doc/integration/saml.md index 8a7205caaa4..f3b2a288776 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -138,7 +138,7 @@ This setting is only available on GitLab 8.7 and above. SAML login includes support for external groups. You can define in the SAML settings which groups, to which your users belong in your IdP, you wish to be -marked as [external](../permissions/permissions.md). +marked as [external](../user/permissions.md). ### Requirements @@ -306,4 +306,4 @@ For this you need take the following into account: validators are optional Make sure that one of the above described scenarios is valid, or the requests will -fail with one of the mentioned errors.
\ No newline at end of file +fail with one of the mentioned errors. diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 44f3f6d3b12..78d67aeec78 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -1,104 +1,3 @@ # Permissions -Users have different abilities depending on the access level they have in a particular group or project. - -If a user is both in a project group and in the project itself, the highest permission level is used. - -If a user is a GitLab administrator they receive all permissions. - -On public and internal projects the Guest role is not enforced. -All users will be able to create issues, leave comments, and pull or download the project code. - -To add or import a user, you can follow the [project users and members -documentation](../workflow/add-user/add-user.md). - -## Project - -| Action | Guest | Reporter | Developer | Master | Owner | -|---------------------------------------|---------|------------|-------------|----------|--------| -| Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ | -| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ | -| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | -| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | -| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | -| Pull project code | | ✓ | ✓ | ✓ | ✓ | -| Download project | | ✓ | ✓ | ✓ | ✓ | -| Create code snippets | | ✓ | ✓ | ✓ | ✓ | -| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | -| Manage labels | | ✓ | ✓ | ✓ | ✓ | -| See a commit status | | ✓ | ✓ | ✓ | ✓ | -| See a container registry | | ✓ | ✓ | ✓ | ✓ | -| See environments | | ✓ | ✓ | ✓ | ✓ | -| Manage merge requests | | | ✓ | ✓ | ✓ | -| Create new merge request | | | ✓ | ✓ | ✓ | -| Create new branches | | | ✓ | ✓ | ✓ | -| Push to non-protected branches | | | ✓ | ✓ | ✓ | -| Force push to non-protected branches | | | ✓ | ✓ | ✓ | -| Remove non-protected branches | | | ✓ | ✓ | ✓ | -| Add tags | | | ✓ | ✓ | ✓ | -| Write a wiki | | | ✓ | ✓ | ✓ | -| Cancel and retry builds | | | ✓ | ✓ | ✓ | -| Create or update commit status | | | ✓ | ✓ | ✓ | -| Update a container registry | | | ✓ | ✓ | ✓ | -| Remove a container registry image | | | ✓ | ✓ | ✓ | -| Create new environments | | | ✓ | ✓ | ✓ | -| Create new milestones | | | | ✓ | ✓ | -| 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 | | | | ✓ | ✓ | -| Configure project hooks | | | | ✓ | ✓ | -| Manage runners | | | | ✓ | ✓ | -| Manage build triggers | | | | ✓ | ✓ | -| Manage variables | | | | ✓ | ✓ | -| Delete environments | | | | ✓ | ✓ | -| Switch visibility level | | | | | ✓ | -| Transfer project to another namespace | | | | | ✓ | -| Remove project | | | | | ✓ | -| Force push to protected branches [^2] | | | | | | -| Remove protected branches [^2] | | | | | | - -[^1]: If **Allow guest to access builds** is enabled in CI settings -[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner - -## 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 | ✓ | ✓ | ✓ | ✓ | ✓ | -| Edit group | | | | | ✓ | -| Create project in group | | | | ✓ | ✓ | -| Manage group members | | | | | ✓ | -| Remove group | | | | | ✓ | - -## External Users - -In cases where it is desired that a user has access only to some internal or -private projects, there is the option of creating **External Users**. This -feature may be useful when for example a contractor is working on a given -project and should only have access to that project. - -External users can only access projects to which they are explicitly granted -access, thus hiding all other internal or private ones from them. Access can be -granted by adding the user as member to the project or group. - -They will, like usual users, receive a role in the project or group with all -the abilities that are mentioned in the table above. They cannot however create -groups or projects, and they have the same access as logged out users in all -other cases. - -An administrator can flag a user as external [through the API](../api/users.md) -or by checking the checkbox on the admin panel. As an administrator, navigate -to **Admin > Users** to create a new user or edit an existing one. There, you -will find the option to flag the user as external. - -By default new users are not set as external users. This behavior can be changed -by an administrator under **Admin > Application Settings**.
\ No newline at end of file +This document was moved to [user/permissions.md](../user/permissions.md). diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md index 9a5c5a5c92a..a3921f1b89f 100644 --- a/doc/public_access/public_access.md +++ b/doc/public_access/public_access.md @@ -17,7 +17,7 @@ Public projects can be cloned **without any** authentication. They will also be listed on the public access directory (`/public`). -**Any logged in user** will have [Guest](../permissions/permissions.md) +**Any logged in user** will have [Guest](../user/permissions.md) permissions on the repository. ### Internal projects @@ -27,7 +27,7 @@ Internal projects can be cloned by any logged in user. They will also be listed on the public access directory (`/public`) for logged in users. -Any logged in user will have [Guest](../permissions/permissions.md) permissions +Any logged in user will have [Guest](../user/permissions.md) permissions on the repository. ### How to change project visibility diff --git a/doc/user/permissions.md b/doc/user/permissions.md new file mode 100644 index 00000000000..66542861781 --- /dev/null +++ b/doc/user/permissions.md @@ -0,0 +1,131 @@ +# Permissions + +Users have different abilities depending on the access level they have in a +particular group or project. If a user is both in a group's project and the +project itself, the highest permission level is used. + +On public and internal projects the Guest role is not enforced. All users will +be able to create issues, leave comments, and pull or download the project code. + +GitLab administrators receive all permissions. + +To add or import a user, you can follow the [project users and members +documentation](../workflow/add-user/add-user.md). + +## Project + +The following table depicts the various user permission levels in a project. + +| Action | Guest | Reporter | Developer | Master | Owner | +|---------------------------------------|---------|------------|-------------|----------|--------| +| Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ | +| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ | +| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| Pull project code | | ✓ | ✓ | ✓ | ✓ | +| Download project | | ✓ | ✓ | ✓ | ✓ | +| Create code snippets | | ✓ | ✓ | ✓ | ✓ | +| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | +| Manage labels | | ✓ | ✓ | ✓ | ✓ | +| See a commit status | | ✓ | ✓ | ✓ | ✓ | +| See a container registry | | ✓ | ✓ | ✓ | ✓ | +| See environments | | ✓ | ✓ | ✓ | ✓ | +| Manage/Accept merge requests | | | ✓ | ✓ | ✓ | +| Create new merge request | | | ✓ | ✓ | ✓ | +| Create new branches | | | ✓ | ✓ | ✓ | +| Push to non-protected branches | | | ✓ | ✓ | ✓ | +| Force push to non-protected branches | | | ✓ | ✓ | ✓ | +| Remove non-protected branches | | | ✓ | ✓ | ✓ | +| Add tags | | | ✓ | ✓ | ✓ | +| Write a wiki | | | ✓ | ✓ | ✓ | +| Cancel and retry builds | | | ✓ | ✓ | ✓ | +| Create or update commit status | | | ✓ | ✓ | ✓ | +| Update a container registry | | | ✓ | ✓ | ✓ | +| Remove a container registry image | | | ✓ | ✓ | ✓ | +| Create new environments | | | ✓ | ✓ | ✓ | +| Create new milestones | | | | ✓ | ✓ | +| Add new team members | | | | ✓ | ✓ | +| Push to protected branches | | | | ✓ | ✓ | +| Enable/disable branch protection | | | | ✓ | ✓ | +| Turn on/off protected branch push for devs| | | | ✓ | ✓ | +| Rewrite/remove Git tags | | | | ✓ | ✓ | +| Edit project | | | | ✓ | ✓ | +| Add deploy keys to project | | | | ✓ | ✓ | +| Configure project hooks | | | | ✓ | ✓ | +| Manage runners | | | | ✓ | ✓ | +| Manage build triggers | | | | ✓ | ✓ | +| Manage variables | | | | ✓ | ✓ | +| Delete environments | | | | ✓ | ✓ | +| Switch visibility level | | | | | ✓ | +| Transfer project to another namespace | | | | | ✓ | +| Remove project | | | | | ✓ | +| Force push to protected branches [^2] | | | | | | +| Remove protected branches [^2] | | | | | | + +[^1]: If **Allow guest to access builds** is enabled in CI settings +[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner + +## Group + +Any user can remove themselves from a group, unless they are the last Owner of +the group. The following table depicts the various user permission levels in a +group. + +| Action | Guest | Reporter | Developer | Master | Owner | +|-------------------------|-------|----------|-----------|--------|-------| +| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ | +| Edit group | | | | | ✓ | +| Create project in group | | | | ✓ | ✓ | +| Manage group members | | | | | ✓ | +| Remove group | | | | | ✓ | + +## External Users + +In cases where it is desired that a user has access only to some internal or +private projects, there is the option of creating **External Users**. This +feature may be useful when for example a contractor is working on a given +project and should only have access to that project. + +External users can only access projects to which they are explicitly granted +access, thus hiding all other internal or private ones from them. Access can be +granted by adding the user as member to the project or group. + +They will, like usual users, receive a role in the project or group with all +the abilities that are mentioned in the table above. They cannot however create +groups or projects, and they have the same access as logged out users in all +other cases. + +An administrator can flag a user as external [through the API](../api/users.md) +or by checking the checkbox on the admin panel. As an administrator, navigate +to **Admin > Users** to create a new user or edit an existing one. There, you +will find the option to flag the user as external. + +By default new users are not set as external users. This behavior can be changed +by an administrator under **Admin > Application Settings**. + +## GitLab CI + +GitLab CI permissions rely on the role the user has in GitLab. There are four +permission levels it total: + +- admin +- master +- developer +- guest/reporter + +The admin user can perform any action on GitLab CI in scope of the GitLab +instance and project. In addition, all admins can use the admin interface under +`/admin/runners`. + +| Action | Guest, Reporter | Developer | Master | Admin | +|---------------------------------------|-----------------|-------------|----------|--------| +| See commits and builds | ✓ | ✓ | ✓ | ✓ | +| Retry or cancel build | | ✓ | ✓ | ✓ | +| Remove project | | | ✓ | ✓ | +| Create project | | | ✓ | ✓ | +| Change project configuration | | | ✓ | ✓ | +| Add specific runners | | | ✓ | ✓ | +| Add shared runners | | | | ✓ | +| See events in the system | | | | ✓ | +| Admin interface | | | | ✓ | diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md index 4b551130255..0537ce0bcd4 100644 --- a/doc/workflow/add-user/add-user.md +++ b/doc/workflow/add-user/add-user.md @@ -23,7 +23,7 @@ want to add. --- -Select the user and the [permission level](../../permissions/permissions.md) +Select the user and the [permission level](../../user/permissions.md) that you'd like to give the user. Note that you can select more than one user. ![Give user permissions](img/add_user_give_permissions.png) diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md index 217a4a4012f..733d079bd4a 100644 --- a/doc/workflow/forking_workflow.md +++ b/doc/workflow/forking_workflow.md @@ -38,7 +38,7 @@ Forking a project is in most cases a two-step process. --- After the forking is done, you can start working on the newly created -repository. There, you will have full [Owner](../permissions/permissions.md) +repository. There, you will have full [Owner](../user/permissions.md) access, so you can set it up as you please. ## Merging upstream diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md index 67adfc2f43a..5c1c7b47c8a 100644 --- a/doc/workflow/protected_branches.md +++ b/doc/workflow/protected_branches.md @@ -12,7 +12,7 @@ A protected branch does three simple things: 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](../permissions/permissions.md). +To protect a branch, user needs to have at least a Master permission level, see [permissions document](../user/permissions.md). ![protected branches page](protected_branches/protected_branches1.png) diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index a95df038357..8b0cb90765e 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -83,11 +83,6 @@ Feature: Project Commits #Given I visit my project's commits stats page #Then I see commits stats - Scenario: I browse big commit - Given I visit big commit page - Then I see big commit warning - And I see "Reload with full diff" link - Scenario: I browse a commit with an image Given I visit a commit with an image that changed Then The diff links to both the previous and current image diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature index 2bde4c8a99b..35687aac9ea 100644 --- a/features/project/commits/diff_comments.feature +++ b/features/project/commits/diff_comments.feature @@ -6,10 +6,6 @@ Feature: Project Commits Diff Comments And I visit project commit page @javascript - Scenario: I can access add diff comment buttons - Then I should see add a diff comment button - - @javascript Scenario: I can comment on a commit diff Given I leave a diff comment like "Typo, please fix" Then I should see a diff comment saying "Typo, please fix" diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb index 800e869533e..9c94dc70df0 100644 --- a/features/steps/dashboard/help.rb +++ b/features/steps/dashboard/help.rb @@ -8,7 +8,7 @@ class Spinach::Features::DashboardHelp < Spinach::FeatureSteps end step 'I visit the "Rake Tasks" help page' do - visit help_page_path("raketasks", "maintenance") + visit help_page_path("raketasks/maintenance") end step 'I should see "Rake Tasks" page markdown rendered' do diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb index 2876e8812e9..b4a32ed2e38 100644 --- a/features/steps/project/builds/artifacts.rb +++ b/features/steps/project/builds/artifacts.rb @@ -68,10 +68,16 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps end step 'download of a file extracted from build artifacts should start' do - # this will be accelerated by Workhorse - response_json = JSON.parse(page.body, symbolize_names: true) - expect(response_json[:archive]).to end_with('build_artifacts.zip') - expect(response_json[:entry]).to eq Base64.encode64('ci_artifacts.txt') + send_data = response_headers[Gitlab::Workhorse::SEND_DATA_HEADER] + + expect(send_data).to start_with('artifacts-entry:') + + base64_params = send_data.sub(/\Aartifacts\-entry:/, '') + params = JSON.parse(Base64.urlsafe_decode64(base64_params)) + + expect(params.keys).to eq(['Archive', 'Entry']) + expect(params['Archive']).to end_with('build_artifacts.zip') + expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt')) end step 'I click a first row within build artifacts table' do diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 239036e431d..bea9f9d198b 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -125,25 +125,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(page).to have_content 'Authors' end - step 'I visit big commit page' do - # Create a temporary scope to ensure that the stub_const is removed after user - RSpec::Mocks.with_temporary_scope do - stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_lines: 1, max_files: 1 }) - visit namespace_project_commit_path(@project.namespace, @project, sample_big_commit.id) - end - end - - step 'I see big commit warning' do - expect(page).to have_content sample_big_commit.message - expect(page).to have_content "Too many changes" - end - - step 'I see "Reload with full diff" link' do - link = find_link('Reload with full diff') - expect(link[:href]).to end_with('?force_show_diff=true') - expect(link[:href]).not_to include('.html') - end - step 'I visit a commit with an image that changed' do visit namespace_project_commit_path(@project.namespace, @project, sample_image_commit.id) end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 56ef44ec969..4df4e89f5b9 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -25,17 +25,16 @@ module SharedDiffNote page.within("form[data-line-code='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Typo, please fix" - find(".js-comment-button").trigger("click") - sleep 0.05 + find(".js-comment-button").click 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[data-line-code='#{sample_commit.line_code}']") do + click_parallel_diff_line(sample_commit.del_line_code, 'old') + page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.del_line_code}']") do fill_in "note[note]", with: "Old comment" - find(".js-comment-button").trigger("click") + find(".js-comment-button").click end end @@ -43,7 +42,7 @@ module SharedDiffNote click_parallel_diff_line(sample_commit.line_code, 'new') page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.line_code}']") do fill_in "note[note]", with: "New comment" - find(".js-comment-button").trigger("click") + find(".js-comment-button").click end end @@ -165,10 +164,6 @@ module SharedDiffNote end end - step 'I should see add a diff comment button' do - expect(page).to have_css('.js-add-diff-note-button') - end - step 'I should see an empty diff comment form' do page.within(diff_file_selector) do expect(page).to have_field("note[note]", with: "") @@ -215,7 +210,7 @@ module SharedDiffNote end step 'I click side-by-side diff button' do - find('#parallel-diff-btn').trigger('click') + find('#parallel-diff-btn').click end step 'I see side-by-side diff button' do @@ -227,10 +222,12 @@ module SharedDiffNote end def click_diff_line(code) - find("button[data-line-code='#{code}']").trigger('click') + find(".line_holder[id='#{code}'] td:nth-of-type(1)").trigger 'mouseover' + find(".line_holder[id='#{code}'] button").trigger 'click' end def click_parallel_diff_line(code, line_type) - find("button[data-line-code='#{code}'][data-line-type='#{line_type}']").trigger('click') + find(".line_content.parallel.#{line_type}[data-line-code='#{code}']").trigger 'mouseover' + find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click' end end diff --git a/features/support/env.rb b/features/support/env.rb index ab3f0ca7aeb..f0a3dd8d2d0 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -13,7 +13,7 @@ require_relative 'rerun' if ENV['CI'] require 'knapsack' - Knapsack::Adapters::RSpecAdapter.bind + Knapsack::Adapters::SpinachAdapter.bind end %w(select2_helper test_env repo_helpers).each do |f| diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index c4fa1838b5a..2efe7e3adf3 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -56,9 +56,9 @@ module API not_found!('Award Emoji') unless can_read_awardable? - award = awardable.award_emoji.new(name: params[:name], user: current_user) + award = awardable.create_award_emoji(params[:name], current_user) - if award.save + if award.persisted? present award, with: Entities::AwardEmoji else not_found!("Award Emoji #{award.errors.messages}") diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 323a7086890..acb4812b5cf 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -64,7 +64,7 @@ module API ref = branches.first end - pipeline = @project.ensure_pipeline(commit.sha, ref) + pipeline = @project.ensure_pipeline(commit.sha, ref, current_user) name = params[:name] || params[:context] status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref]) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 9076a0c3831..3c79a00eb8c 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -54,6 +54,7 @@ module API class BasicProjectDetails < Grape::Entity expose :id + expose :http_url_to_repo, :web_url expose :name, :name_with_namespace expose :path, :path_with_namespace end @@ -186,6 +187,7 @@ module API end expose :user_notes_count expose :upvotes, :downvotes + expose :due_date end class ExternalIssue < Grape::Entity @@ -199,7 +201,6 @@ module API expose :author, :assignee, using: Entities::UserBasic expose :source_project_id, :target_project_id expose :label_names, as: :labels - expose :description expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone expose :merge_when_build_succeeds @@ -208,6 +209,8 @@ module API merge_request.subscribed?(options[:current_user]) end expose :user_notes_count + expose :should_remove_source_branch?, as: :should_remove_source_branch + expose :force_remove_source_branch?, as: :force_remove_source_branch end class MergeRequestChanges < MergeRequest diff --git a/lib/api/internal.rb b/lib/api/internal.rb index d5dfba5e0cc..959b700de78 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -63,7 +63,12 @@ module API if access_status.status # Return the repository full path so that gitlab-shell has it when # handling ssh commands - response[:repository_path] = project.repository.path_to_repo + response[:repository_path] = + if wiki? + project.wiki.repository.path_to_repo + else + project.repository.path_to_repo + end end response diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 8a03a41e9c5..c588103e517 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -152,12 +152,13 @@ module API # milestone_id (optional) - The ID of a milestone to assign issue # labels (optional) - The labels of an issue # created_at (optional) - Date time string, ISO 8601 formatted + # due_date (optional) - Date time string in the format YEAR-MONTH-DAY # Example Request: # POST /projects/:id/issues - post ":id/issues" do + post ':id/issues' do required_attributes! [:title] - keys = [:title, :description, :assignee_id, :milestone_id] + keys = [:title, :description, :assignee_id, :milestone_id, :due_date] keys << :created_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) @@ -201,12 +202,13 @@ module API # labels (optional) - The labels of an issue # state_event (optional) - The state event of an issue (close|reopen) # updated_at (optional) - Date time string, ISO 8601 formatted + # due_date (optional) - Date time string in the format YEAR-MONTH-DAY # Example Request: # PUT /projects/:id/issues/:issue_id - put ":id/issues/:issue_id" do + put ':id/issues/:issue_id' do issue = user_project.issues.find(params[:issue_id]) authorize! :update_issue, issue - keys = [:title, :description, :assignee_id, :milestone_id, :state_event] + keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date] keys << :updated_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 0cc1edd65c8..6d2a6f3946c 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -25,7 +25,11 @@ module API @projects = current_user.authorized_projects @projects = filter_projects(@projects) @projects = paginate @projects - present @projects, with: Entities::ProjectWithAccess, user: current_user + if params[:simple] + present @projects, with: Entities::BasicProjectDetails, user: current_user + else + present @projects, with: Entities::ProjectWithAccess, user: current_user + end end # Get an owned projects list for authenticated user diff --git a/lib/banzai.rb b/lib/banzai.rb index 093382261ae..9ebe379f454 100644 --- a/lib/banzai.rb +++ b/lib/banzai.rb @@ -3,6 +3,10 @@ module Banzai Renderer.render(text, context) end + def self.cache_collection_render(texts_and_contexts) + Renderer.cache_collection_render(texts_and_contexts) + end + def self.render_result(text, context = {}) Renderer.render_result(text, context) end diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index 536b478979f..91f0159f9a1 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -19,14 +19,22 @@ module Banzai language = node.attr('class') code = node.text + css_classes = "code highlight" + + lexer = Rouge::Lexer.find_fancy(language) || Rouge::Lexers::PlainText + formatter = Rouge::Formatters::HTML.new + begin - highlighted = block_code(code, language) + code = formatter.format(lexer.lex(code)) + + css_classes << " js-syntax-highlight #{lexer.tag}" rescue # Gracefully handle syntax highlighter bugs/errors to ensure # users can still access an issue/comment/etc. - highlighted = "<pre>#{code}</pre>" end + highlighted = %(<pre class="#{css_classes}"><code>#{code}</code></pre>) + # Extracted to a method to measure it replace_parent_pre_element(node, highlighted) end @@ -40,8 +48,7 @@ module Banzai # Override Rouge::Plugins::Redcarpet#rouge_formatter def rouge_formatter(lexer) - Rouge::Formatters::HTMLGitlab.new( - cssclass: "code highlight js-syntax-highlight #{lexer.tag}") + Rouge::Formatters::HTML.new end end end diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index 5b0a6d8541b..e1ca7f4d24b 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -112,7 +112,7 @@ module Banzai data = data_attribute(project: project.id, author: author.try(:id)) text = link_text || User.reference_prefix + 'all' - link_tag(url, data, text) + link_tag(url, data, text, 'All Project and Group Members') end def link_to_namespace(namespace, link_text: nil) @@ -128,7 +128,7 @@ module Banzai data = data_attribute(group: namespace.id) text = link_text || Group.reference_prefix + group - link_tag(url, data, text) + link_tag(url, data, text, namespace.name) end def link_to_user(user, namespace, link_text: nil) @@ -136,11 +136,11 @@ module Banzai data = data_attribute(user: namespace.owner_id) text = link_text || User.reference_prefix + user - link_tag(url, data, text) + link_tag(url, data, text, namespace.owner_name) end - def link_tag(url, data, text) - %(<a href="#{url}" #{data} class="#{link_class}">#{escape_once(text)}</a>) + def link_tag(url, data, text, title) + %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{escape_once(text)}</a>) end end end diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb index f0e4f28bf12..9aef807c152 100644 --- a/lib/banzai/object_renderer.rb +++ b/lib/banzai/object_renderer.rb @@ -31,17 +31,15 @@ module Banzai redacted = redact_documents(documents) objects.each_with_index do |object, index| - object.__send__("#{attribute}_html=", redacted.fetch(index)) + redacted_data = redacted[index] + object.__send__("#{attribute}_html=", redacted_data[:document].to_html.html_safe) + object.user_visible_reference_count = redacted_data[:visible_reference_count] end - - objects end # Renders the attribute of every given object. def render_objects(objects, attribute) - objects.map do |object| - render_attribute(object, attribute) - end + render_attributes(objects, attribute) end # Redacts the list of documents. @@ -50,9 +48,7 @@ module Banzai def redact_documents(documents) redactor = Redactor.new(project, user) - redactor.redact(documents).map do |document| - document.to_html.html_safe - end + redactor.redact(documents) end # Returns a Banzai context for the given object and attribute. @@ -66,16 +62,21 @@ module Banzai context end - # Renders the attribute of an object. + # Renders the attributes of a set of objects. # - # Returns a `Nokogiri::HTML::Document`. - def render_attribute(object, attribute) - context = context_for(object, attribute) + # Returns an Array of `Nokogiri::HTML::Document`. + def render_attributes(objects, attribute) + strings_and_contexts = objects.map do |object| + context = context_for(object, attribute) - string = object.__send__(attribute) - html = Banzai.render(string, context) + string = object.__send__(attribute) - Banzai::Pipeline[:relative_link].to_document(html, context) + { text: string, context: context } + end + + Banzai.cache_collection_render(strings_and_contexts).each_with_index.map do |html, index| + Banzai::Pipeline[:relative_link].to_document(html, strings_and_contexts[index][:context]) + end end def base_context diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb index ffd267d5e9a..0df3a72d1c4 100644 --- a/lib/banzai/redactor.rb +++ b/lib/banzai/redactor.rb @@ -19,29 +19,36 @@ module Banzai # # Returns the documents passed as the first argument. def redact(documents) - nodes = documents.flat_map do |document| - Querying.css(document, 'a.gfm[data-reference-type]') - end - - redact_nodes(nodes) + all_document_nodes = document_nodes(documents) - documents + redact_document_nodes(all_document_nodes) end - # Redacts the given nodes + # Redacts the given node documents # - # nodes - An Array of HTML nodes to redact. - def redact_nodes(nodes) - visible = nodes_visible_to_user(nodes) + # data - An Array of a Hashes mapping an HTML document to nodes to redact. + def redact_document_nodes(all_document_nodes) + all_nodes = all_document_nodes.map { |x| x[:nodes] }.flatten + visible = nodes_visible_to_user(all_nodes) + metadata = [] - nodes.each do |node| - unless visible.include?(node) + all_document_nodes.each do |entry| + nodes_for_document = entry[:nodes] + doc_data = { document: entry[:document], visible_reference_count: nodes_for_document.count } + metadata << doc_data + + nodes_for_document.each do |node| + next if visible.include?(node) + + doc_data[:visible_reference_count] -= 1 # The reference should be replaced by the original text, # which is not always the same as the rendered text. text = node.attr('data-original') || node.text node.replace(text) end end + + metadata end # Returns the nodes visible to the current user. @@ -65,5 +72,11 @@ module Banzai visible end + + def document_nodes(documents) + documents.map do |document| + { document: document, nodes: Querying.css(document, 'a.gfm[data-reference-type]') } + end + end end end diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index 6718acdef7e..910687a7b6a 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -10,7 +10,7 @@ module Banzai # requiring XHTML, such as Atom feeds, need to call `post_process` on the # result, providing the appropriate `pipeline` option. # - # markdown - Markdown String + # text - Markdown String # context - Hash of context options passed to our HTML Pipeline # # Returns an HTML-safe String @@ -29,6 +29,58 @@ module Banzai end end + # Perform multiple render from an Array of Markdown String into an + # Array of HTML-safe String of HTML. + # + # As the rendered Markdown String can be already cached read all the data + # from the cache using Rails.cache.read_multi operation. If the Markdown String + # is not in the cache or it's not cacheable (no cache_key entry is provided in + # the context) the Markdown String is rendered and stored in the cache so the + # next render call gets the rendered HTML-safe String from the cache. + # + # For further explanation see #render method comments. + # + # texts_and_contexts - An Array of Hashes that contains the Markdown String (:text) + # an options passed to our HTML Pipeline (:context) + # + # If on the :context you specify a :cache_key entry will be used to retrieve it + # and cache the result of rendering the Markdown String. + # + # Returns an Array containing HTML-safe String instances. + # + # Example: + # texts_and_contexts + # => [{ text: '### Hello', + # context: { cache_key: [note, :note] } }] + def self.cache_collection_render(texts_and_contexts) + items_collection = texts_and_contexts.each_with_index do |item, index| + context = item[:context] + cache_key = full_cache_multi_key(context.delete(:cache_key), context[:pipeline]) + + item[:cache_key] = cache_key if cache_key + end + + cacheable_items, non_cacheable_items = items_collection.partition { |item| item.key?(:cache_key) } + + items_in_cache = [] + items_not_in_cache = [] + + unless cacheable_items.empty? + items_in_cache = Rails.cache.read_multi(*cacheable_items.map { |item| item[:cache_key] }) + items_not_in_cache = cacheable_items.reject do |item| + item[:rendered] = items_in_cache[item[:cache_key]] + items_in_cache.key?(item[:cache_key]) + end + end + + (items_not_in_cache + non_cacheable_items).each do |item| + item[:rendered] = render(item[:text], item[:context]) + Rails.cache.write(item[:cache_key], item[:rendered]) if item[:cache_key] + end + + items_collection.map { |item| item[:rendered] } + end + def self.render_result(text, context = {}) text = Pipeline[:pre_process].to_html(text, context) if text @@ -78,5 +130,13 @@ module Banzai return unless cache_key ["banzai", *cache_key, pipeline_name || :full] end + + # To map Rails.cache.read_multi results we need to know the Rails.cache.expanded_key. + # Other option will be to generate stringified keys on our side and don't delegate to Rails.cache.expanded_key + # method. + def self.full_cache_multi_key(cache_key, pipeline_name) + return unless cache_key + Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name)) + end end end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 01ef13df57a..a48dc542b14 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -31,28 +31,34 @@ module Ci raise ValidationError, e.message end - def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil) - builds.select do |build| - build[:stage] == stage && - process?(build[:only], build[:except], ref, tag, trigger_request) + def jobs_for_ref(ref, tag = false, trigger_request = nil) + @jobs.select do |_, job| + process?(job[:only], job[:except], ref, tag, trigger_request) end end - def builds - @jobs.map do |name, job| - build_job(name, job) + def jobs_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil) + jobs_for_ref(ref, tag, trigger_request).select do |_, job| + job[:stage] == stage end end - def global_variables - @variables + def builds_for_ref(ref, tag = false, trigger_request = nil) + jobs_for_ref(ref, tag, trigger_request).map do |name, job| + build_job(name, job) + end end - def job_variables(name) - job = @jobs[name.to_sym] - return [] unless job + def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil) + jobs_for_stage_and_ref(stage, ref, tag, trigger_request).map do |name, job| + build_job(name, job) + end + end - job[:variables] || [] + def builds + @jobs.map do |name, job| + build_job(name, job) + end end private @@ -95,11 +101,10 @@ module Ci commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"), tag_list: job[:tags] || [], name: name, - only: job[:only], - except: job[:except], allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment], + yaml_variables: yaml_variables(name), options: { image: job[:image] || @image, services: job[:services] || @services, @@ -111,6 +116,24 @@ module Ci } end + def yaml_variables(name) + variables = global_variables.merge(job_variables(name)) + variables.map do |key, value| + { key: key, value: value, public: true } + end + end + + def global_variables + @variables || {} + end + + def job_variables(name) + job = @jobs[name.to_sym] + return {} unless job + + job[:variables] || {} + end + def validate! @jobs.each do |name, job| validate_job!(name, job) diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index 42232b7129d..2edddb84fc3 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -7,62 +7,91 @@ module ContainerRegistry MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json' + # Taken from: FaradayMiddleware::FollowRedirects + REDIRECT_CODES = Set.new [301, 302, 303, 307] + def initialize(base_uri, options = {}) @base_uri = base_uri - @faraday = Faraday.new(@base_uri) do |conn| - initialize_connection(conn, options) - end + @options = options end def repository_tags(name) - response_body @faraday.get("/v2/#{name}/tags/list") + response_body faraday.get("/v2/#{name}/tags/list") end def repository_manifest(name, reference) - response_body @faraday.get("/v2/#{name}/manifests/#{reference}") + response_body faraday.get("/v2/#{name}/manifests/#{reference}") end def repository_tag_digest(name, reference) - response = @faraday.head("/v2/#{name}/manifests/#{reference}") + response = faraday.head("/v2/#{name}/manifests/#{reference}") response.headers['docker-content-digest'] if response.success? end def delete_repository_tag(name, reference) - @faraday.delete("/v2/#{name}/manifests/#{reference}").success? + faraday.delete("/v2/#{name}/manifests/#{reference}").success? end def blob(name, digest, type = nil) - headers = {} - headers['Accept'] = type if type - response_body @faraday.get("/v2/#{name}/blobs/#{digest}", nil, headers) + type ||= 'application/octet-stream' + response_body faraday_blob.get("/v2/#{name}/blobs/#{digest}", nil, 'Accept' => type), allow_redirect: true end def delete_blob(name, digest) - @faraday.delete("/v2/#{name}/blobs/#{digest}").success? + faraday.delete("/v2/#{name}/blobs/#{digest}").success? end - + private - + def initialize_connection(conn, options) conn.request :json + + if options[:user] && options[:password] + conn.request(:basic_auth, options[:user].to_s, options[:password].to_s) + elsif options[:token] + conn.request(:authorization, :bearer, options[:token].to_s) + end + + conn.adapter :net_http + end + + def accept_manifest(conn) conn.headers['Accept'] = MANIFEST_VERSION conn.response :json, content_type: 'application/json' conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+prettyjws' conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+json' conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v2+json' + end - if options[:user] && options[:password] - conn.request(:basic_auth, options[:user].to_s, options[:password].to_s) - elsif options[:token] - conn.request(:authorization, :bearer, options[:token].to_s) + def response_body(response, allow_redirect: false) + if allow_redirect && REDIRECT_CODES.include?(response.status) + response = redirect_response(response.headers['location']) end - conn.adapter :net_http + response.body if response && response.success? + end + + def redirect_response(location) + return unless location + + # We explicitly remove authorization token + faraday_blob.get(location) do |req| + req['Authorization'] = '' + end end - def response_body(response) - response.body if response.success? + def faraday + @faraday ||= Faraday.new(@base_uri) do |conn| + initialize_connection(conn, @options) + accept_manifest(conn) + end + end + + def faraday_blob + @faraday_blob ||= Faraday.new(@base_uri) do |conn| + initialize_connection(conn, @options) + end end end end diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb index 708d01b95a1..59040199920 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -53,7 +53,7 @@ module ContainerRegistry def config return unless config_blob - @config ||= ContainerRegistry::Config.new(self, config_blob) + @config ||= ContainerRegistry::Config.new(self, config_blob) if config_blob.data end def created_at diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 478f145bfed..ab94abeda77 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -63,7 +63,7 @@ module Grack def ci_request?(login, password) matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login) - if project && matched_login.present? && git_cmd == 'git-upload-pack' + if project && matched_login.present? underscored_service = matched_login['s'].underscore if underscored_service == 'gitlab_ci' diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index dec20d8659b..927f9dad20b 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -20,11 +20,19 @@ module Gitlab if Database.postgresql? options = options.merge({ algorithm: :concurrently }) + disable_statement_timeout end add_index(table_name, column_name, options) end + # Long-running migrations may take more than the timeout allowed by + # the database. Disable the session's statement timeout to ensure + # migrations don't get killed prematurely. (PostgreSQL only) + def disable_statement_timeout + ActiveRecord::Base.connection.execute('SET statement_timeout TO 0') if Database.postgresql? + end + # Updates the value of a column in batches. # # This method updates the table in batches of 5% of the total row count. @@ -133,6 +141,8 @@ module Gitlab 'in the body of your migration class' end + disable_statement_timeout + transaction do add_column(table, column, type, default: nil) diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index b0c50edba59..7e01f7b61fb 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -68,6 +68,10 @@ module Gitlab @lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a end + def collapsed_by_default? + diff.diff.bytesize > 10240 # 10 KB + end + def highlighted_diff_lines @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight end diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb index 789c14518b0..28ad637fda4 100644 --- a/lib/gitlab/diff/inline_diff.rb +++ b/lib/gitlab/diff/inline_diff.rb @@ -1,16 +1,30 @@ module Gitlab module Diff class InlineDiff + # Regex to find a run of deleted lines followed by the same number of added lines + LINE_PAIRS_PATTERN = %r{ + # Runs start at the beginning of the string (the first line) or after a space (for an unchanged line) + (?:\A|\s) + + # This matches a number of `-`s followed by the same number of `+`s through recursion + (?<del_ins> + - + \g<del_ins>? + \+ + ) + + # Runs end at the end of the string (the last line) or before a space (for an unchanged line) + (?=\s|\z) + }x.freeze + attr_accessor :old_line, :new_line, :offset def self.for_lines(lines) - local_edit_indexes = self.find_local_edits(lines) + changed_line_pairs = self.find_changed_line_pairs(lines) inline_diffs = [] - local_edit_indexes.each do |index| - old_index = index - new_index = index + 1 + changed_line_pairs.each do |old_index, new_index| old_line = lines[old_index] new_line = lines[new_index] @@ -51,18 +65,28 @@ module Gitlab private - def self.find_local_edits(lines) - line_prefixes = lines.map { |line| line.match(/\A([+-])/) ? $1 : ' ' } - joined_line_prefixes = " #{line_prefixes.join} " - - offset = 0 - local_edit_indexes = [] - while index = joined_line_prefixes.index(" -+ ", offset) - local_edit_indexes << index - offset = index + 1 + # Finds pairs of old/new line pairs that represent the same line that changed + def self.find_changed_line_pairs(lines) + # Prefixes of all diff lines, indicating their types + # For example: `" - + -+ ---+++ --+ -++"` + line_prefixes = lines.each_with_object("") { |line, s| s << line[0] }.gsub(/[^ +-]/, ' ') + + changed_line_pairs = [] + line_prefixes.scan(LINE_PAIRS_PATTERN) do + # For `"---+++"`, `begin_index == 0`, `end_index == 6` + begin_index, end_index = Regexp.last_match.offset(:del_ins) + + # For `"---+++"`, `changed_line_count == 3` + changed_line_count = (end_index - begin_index) / 2 + + halfway_index = begin_index + changed_line_count + (begin_index...halfway_index).each do |i| + # For `"---+++"`, index 1 maps to 1 + 3 = 4 + changed_line_pairs << [i, i + changed_line_count] + end end - local_edit_indexes + changed_line_pairs end def longest_common_prefix(a, b) diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb index 1c1fc148123..b069afdd28c 100644 --- a/lib/gitlab/diff/parallel_diff.rb +++ b/lib/gitlab/diff/parallel_diff.rb @@ -8,95 +8,78 @@ module Gitlab end def parallelize - lines = [] - skip_next = false + i = 0 + free_right_index = nil + + lines = [] highlighted_diff_lines = diff_file.highlighted_diff_lines highlighted_diff_lines.each do |line| - full_line = line.text - type = line.type line_code = diff_file.line_code(line) - line_new = line.new_pos - line_old = line.old_pos position = diff_file.position(line) - next_line = diff_file.next_line(line.index) - - if next_line - next_line = highlighted_diff_lines[next_line.index] - full_next_line = next_line.text - next_line_code = diff_file.line_code(next_line) - next_type = next_line.type - next_position = diff_file.position(next_line) - end - - case type + case line.type when 'match', nil # line in the right panel is the same as in the left one lines << { left: { - type: type, - number: line_old, - text: full_line, + type: line.type, + number: line.old_pos, + text: line.text, line_code: line_code, position: position }, right: { - type: type, - number: line_new, - text: full_line, + type: line.type, + number: line.new_pos, + text: line.text, line_code: line_code, position: position } } + + free_right_index = nil + i += 1 when 'old' - case next_type - when 'new' - # Left side has text removed, right side has text added - lines << { - left: { - type: type, - number: line_old, - text: full_line, - line_code: line_code, - position: position - }, - right: { - type: next_type, - number: line_new, - text: full_next_line, - line_code: next_line_code, - position: next_position, - } - } - skip_next = true - when 'old', 'nonewline', nil - # Left side has text removed, right side doesn't have any change - # No next line code, no new line number, no new line text - lines << { - left: { - type: type, - number: line_old, - text: full_line, - line_code: line_code, - position: position - }, - right: { - type: next_type, - number: nil, - text: "", - line_code: nil, - position: nil - } + lines << { + left: { + type: line.type, + number: line.old_pos, + text: line.text, + line_code: line_code, + position: position + }, + right: { + type: nil, + number: nil, + text: "", + line_code: line_code, + position: position } - end + } + + # Once we come upon a new line it can be put on the right of this old line + free_right_index ||= i + i += 1 when 'new' - if skip_next - # Change has been already included in previous line so no need to do it again - skip_next = false - next + data = { + type: line.type, + number: line.new_pos, + text: line.text, + line_code: line_code, + position: position + } + + if free_right_index + # If an old line came before this without a line on the right, this + # line can be put to the right of it. + lines[free_right_index][:right] = data + + # If there are any other old lines on the left that don't yet have + # a new counterpart on the right, update the free_right_index + next_free_right_index = free_right_index + 1 + free_right_index = next_free_right_index < i ? next_free_right_index : nil else - # Change is only on the right side, left side has no change lines << { left: { type: nil, @@ -105,17 +88,15 @@ module Gitlab line_code: line_code, position: position }, - right: { - type: type, - number: line_new, - text: full_line, - line_code: line_code, - position: position - } + right: data } + + free_right_index = nil + i += 1 end end end + lines end end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 043f10d96a9..084e514492c 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -78,10 +78,21 @@ module Gitlab def rate_limit api.rate_limit! + # GitHub Rate Limit API returns 404 when the rate limit is + # disabled. In this case we just want to return gracefully + # instead of spitting out an error. + rescue Octokit::NotFound + nil + end + + def has_rate_limit? + return @has_rate_limit if defined?(@has_rate_limit) + + @has_rate_limit = rate_limit.present? end def rate_limit_exceed? - rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS + has_rate_limit? && rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS end def rate_limit_sleep_time diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb index 3f76ec97977..46d40f75be6 100644 --- a/lib/gitlab/gitlab_import/importer.rb +++ b/lib/gitlab/gitlab_import/importer.rb @@ -15,31 +15,35 @@ module Gitlab end def execute - project_identifier = CGI.escape(project.import_source) - - # Issues && Comments - issues = client.issues(project_identifier) - - issues.each do |issue| - body = @formatter.author_line(issue["author"]["name"]) - body += issue["description"] - - comments = client.issue_comments(project_identifier, issue["id"]) - - if comments.any? - body += @formatter.comments_header + ActiveRecord::Base.no_touching do + project_identifier = CGI.escape(project.import_source) + + # Issues && Comments + issues = client.issues(project_identifier) + + issues.each do |issue| + body = @formatter.author_line(issue["author"]["name"]) + body += 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!( + iid: issue["iid"], + description: body, + title: issue["title"], + state: issue["state"], + updated_at: issue["updated_at"], + author_id: gl_user_id(project, issue["author"]["id"]) + ) 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 diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 41296415e35..9360afedfcb 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -1,7 +1,7 @@ module Gitlab class Highlight - def self.highlight(blob_name, blob_content, repository: nil, nowrap: true, plain: false) - new(blob_name, blob_content, nowrap: nowrap, repository: repository). + def self.highlight(blob_name, blob_content, repository: nil, plain: false) + new(blob_name, blob_content, repository: repository). highlight(blob_content, continue: false, plain: plain) end @@ -13,30 +13,34 @@ module Gitlab highlight(file_name, blob.data, repository: repository).lines.map!(&:html_safe) end - attr_reader :lexer - def initialize(blob_name, blob_content, repository: nil, nowrap: true) + def initialize(blob_name, blob_content, repository: nil) + @formatter = Rouge::Formatters::HTMLGitlab.new + @repository = repository @blob_name = blob_name @blob_content = blob_content - @repository = repository - @formatter = rouge_formatter(nowrap: nowrap) - - @lexer = custom_language || begin - Rouge::Lexer.guess(filename: blob_name, source: blob_content).new - rescue Rouge::Lexer::AmbiguousGuess => e - e.alternatives.sort_by(&:tag).first - end end def highlight(text, continue: true, plain: false) if plain - @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe + hl_lexer = Rouge::Lexers::PlainText + continue = false else - @formatter.format(@lexer.lex(text, continue: continue)).html_safe + hl_lexer = self.lexer end + + @formatter.format(hl_lexer.lex(text, continue: continue)).html_safe rescue @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe end + def lexer + @lexer ||= custom_language || begin + Rouge::Lexer.guess(filename: @blob_name, source: @blob_content).new + rescue Rouge::Guesser::Ambiguous => e + e.alternatives.sort_by(&:tag).first + end + end + private def custom_language @@ -46,16 +50,5 @@ module Gitlab Rouge::Lexer.find_fancy(language_name) end - - def rouge_formatter(options = {}) - options = options.reverse_merge( - nowrap: true, - cssclass: 'code highlight', - lineanchors: true, - lineanchorsid: 'LC' - ) - - Rouge::Formatters::HTMLGitlab.new(options) - end end end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 588647e5adb..bab2ea73c4f 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -3,6 +3,7 @@ module Gitlab extend self VERSION = '0.1.1' + FILENAME_LIMIT = 50 def export_path(relative_path:) File.join(storage_path, relative_path) @@ -28,6 +29,12 @@ module Gitlab 'VERSION' end + def export_filename(project:) + basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.namespace.path}_#{project.path}" + + "#{basename[0..FILENAME_LIMIT]}_export.tar.gz" + end + def version VERSION end diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index 8f66f48cbfe..6b69a653f12 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -44,8 +44,7 @@ module Gitlab def wiki_restorer Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path, shared: @shared, - project: ProjectWiki.new(project_tree.restored_project), - wiki: true) + project: ProjectWiki.new(project_tree.restored_project)) end def uploads_restorer diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 0ac6ff01e3b..051110c23cf 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -12,7 +12,10 @@ module Gitlab json = IO.read(@path) @tree_hash = ActiveSupport::JSON.decode(json) @project_members = @tree_hash.delete('project_members') - create_relations + + ActiveRecord::Base.no_touching do + create_relations + end rescue => e @shared.error(e) false @@ -69,10 +72,19 @@ module Gitlab # Example: # +relation_key+ issues, loops through the list of *issues* and for each individual # issue, finds any subrelations such as notes, creates them and assign them back to the hash + # + # Recursively calls this method if the sub-relation is a hash containing more sub-relations def create_sub_relations(relation, tree_hash) relation_key = relation.keys.first.to_s + return if tree_hash[relation_key].blank? + tree_hash[relation_key].each do |relation_item| relation.values.flatten.each do |sub_relation| + # We just use author to get the user ID, do not attempt to create an instance. + next if sub_relation == :author + + create_sub_relations(sub_relation, relation_item) if sub_relation.is_a?(Hash) + relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation) relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank? end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 9824df3f274..6ba25a31641 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -87,7 +87,7 @@ module Gitlab project_id = @relation_hash.delete('project_id') # project_id may not be part of the export, but we always need to populate it if required. - @relation_hash['project_id'] = project_id if relation_class.column_names.include?('project_id') + @relation_hash['project_id'] = project_id @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id'] @relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id'] @relation_hash['source_project_id'] = -1 if @relation_hash['source_project_id'] @@ -111,7 +111,7 @@ module Gitlab end def imported_object - imported_object = relation_class.new(@relation_hash) + imported_object = relation_class.new(parsed_relation_hash) yield(imported_object) if block_given? imported_object.importing = true if imported_object.respond_to?(:importing) imported_object @@ -125,6 +125,10 @@ module Gitlab def admin_user? @user.is_admin? end + + def parsed_relation_hash + @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) } + end end end end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 546dae4d122..f84de652a57 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -3,15 +3,14 @@ module Gitlab class RepoRestorer include Gitlab::ImportExport::CommandLineUtil - def initialize(project:, shared:, path_to_bundle:, wiki: false) + def initialize(project:, shared:, path_to_bundle:) @project = project @path_to_bundle = path_to_bundle @shared = shared - @wiki = wiki end def restore - return wiki? unless File.exist?(@path_to_bundle) + return true unless File.exist?(@path_to_bundle) FileUtils.mkdir_p(path_to_repo) @@ -30,10 +29,6 @@ module Gitlab def path_to_repo @project.repository.path_to_repo end - - def wiki? - @wiki - end end end end diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb index cce43fe994b..331e14021e6 100644 --- a/lib/gitlab/import_export/repo_saver.rb +++ b/lib/gitlab/import_export/repo_saver.rb @@ -11,7 +11,7 @@ module Gitlab end def save - return false if @project.empty_repo? + return true if @project.empty_repo? # it's ok to have no repo @full_path = File.join(@shared.export_path, ImportExport.project_bundle_filename) bundle_to_disk diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index 6a60b65071f..6130c124dd1 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -7,7 +7,8 @@ module Gitlab new(*args).save end - def initialize(shared:) + def initialize(project:, shared:) + @project = project @shared = shared end @@ -36,7 +37,7 @@ module Gitlab end def archive_file - @archive_file ||= File.join(@shared.export_path, '..', "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_project_export.tar.gz") + @archive_file ||= File.join(@shared.export_path, '..', Gitlab::ImportExport.export_filename(project: @project)) end end end diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb index 1eedae39f8a..6107420e4dd 100644 --- a/lib/gitlab/import_export/wiki_repo_saver.rb +++ b/lib/gitlab/import_export/wiki_repo_saver.rb @@ -4,6 +4,7 @@ module Gitlab def save @wiki = ProjectWiki.new(@project) return true unless wiki_repository_exists? # it's okay to have no Wiki + bundle_to_disk(File.join(@shared.export_path, project_filename)) end diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb index 811363405a8..a1ee1aa81ff 100644 --- a/lib/gitlab/lfs/response.rb +++ b/lib/gitlab/lfs/response.rb @@ -47,6 +47,8 @@ module Gitlab end def render_storage_upload_store_response(oid, size, tmp_file_name) + return render_forbidden unless tmp_file_name + render_response_to_push do render_lfs_upload_ok(oid, size, tmp_file_name) end diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb index 69bd5e62305..f2a76a56b8f 100644 --- a/lib/gitlab/lfs/router.rb +++ b/lib/gitlab/lfs/router.rb @@ -74,8 +74,6 @@ module Gitlab lfs.render_storage_upload_authorize_response(oid, size) else tmp_file_name = sanitize_tmp_filename(@request.env['HTTP_X_GITLAB_LFS_TMP']) - return nil unless tmp_file_name - lfs.render_storage_upload_store_response(oid, size, tmp_file_name) end end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index bc0193a6c32..6aeb49c0219 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -63,6 +63,18 @@ module Gitlab ] end + def send_artifacts_entry(build, entry) + params = { + 'Archive' => build.artifacts_file.path, + 'Entry' => Base64.encode64(entry.path) + } + + [ + SEND_DATA_HEADER, + "artifacts-entry:#{encode(params)}" + ] + end + protected def encode(hash) diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb index 3358ed6773e..f818dc78d34 100644 --- a/lib/rouge/formatters/html_gitlab.rb +++ b/lib/rouge/formatters/html_gitlab.rb @@ -1,171 +1,27 @@ -require 'cgi' - module Rouge module Formatters - class HTMLGitlab < Rouge::Formatter + class HTMLGitlab < Rouge::Formatters::HTML tag 'html_gitlab' # Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance. # - # [+nowrap+] If set to True, don't wrap the output at all, not - # even inside a <tt><pre></tt> tag (default: false). - # [+cssclass+] CSS class for the wrapping <tt><div></tt> tag - # (default: 'highlight'). - # [+linenos+] If set to 'table', output line numbers as a table - # with two cells, one containing the line numbers, - # the other the whole code. This is copy paste friendly, - # but may cause alignment problems with some browsers - # or fonts. If set to 'inline', the line numbers will - # be integrated in the <tt><pre></tt> tag that contains - # the code (default: nil). # [+linenostart+] The line number for the first line (default: 1). - # [+lineanchors+] If set to true the formatter will wrap each output - # line in an anchor tag with a name of L-linenumber. - # This allows easy linking to certain lines - # (default: false). - # [+lineanchorsid+] If lineanchors is true the name of the anchors can - # be changed with lineanchorsid to e.g. foo-linenumber - # (default: 'L'). - # [+anchorlinenos+] If set to true, will wrap line numbers in <tt><a></tt> - # tags. Used in combination with linenos and lineanchors - # (default: false). - # [+inline_theme+] Inline CSS styles for the <pre> tag (default: false). - def initialize( - nowrap: false, - cssclass: 'highlight', - linenos: nil, - linenostart: 1, - lineanchors: false, - lineanchorsid: 'L', - anchorlinenos: false, - inline_theme: nil - ) - @nowrap = nowrap - @cssclass = cssclass - @linenos = linenos + def initialize(linenostart: 1) @linenostart = linenostart - @lineanchors = lineanchors - @lineanchorsid = lineanchorsid - @anchorlinenos = anchorlinenos - @inline_theme = Theme.find(inline_theme).new if inline_theme.is_a?(String) - end - - def render(tokens) - case @linenos - when 'table' - render_tableized(tokens) - when 'inline' - render_untableized(tokens) - else - render_untableized(tokens) - end - end - - alias_method :format, :render - - private - - def render_untableized(tokens) - data = process_tokens(tokens) - - html = '' - html << "<pre class=\"#{@cssclass}\"><code>" unless @nowrap - html << wrap_lines(data[:code]) - html << "</code></pre>\n" unless @nowrap - html + @line_number = linenostart end - def render_tableized(tokens) - data = process_tokens(tokens) - - html = '' - html << "<div class=\"#{@cssclass}\">" unless @nowrap - html << '<table><tbody>' - html << "<td class=\"linenos\"><pre>" - html << wrap_linenos(data[:numbers]) - html << '</pre></td>' - html << "<td class=\"lines\"><pre><code>" - html << wrap_lines(data[:code]) - html << '</code></pre></td>' - html << '</tbody></table>' - html << '</div>' unless @nowrap - html - end - - def process_tokens(tokens) - rendered = [] - current_line = '' - - tokens.each do |tok, val| - # In the case of multi-line values (e.g. comments), we need to apply - # styling to each line since span elements are inline. - val.lines.each do |line| - stripped = line.chomp - current_line << span(tok, stripped) - - if line.end_with?("\n") - rendered << current_line - current_line = '' - end - end - end - - # Add leftover text - rendered << current_line if current_line.present? - - num_lines = rendered.size - numbers = (@linenostart..num_lines + @linenostart - 1).to_a - - { numbers: numbers, code: rendered } - end - - def wrap_linenos(numbers) - if @anchorlinenos - numbers.map! do |number| - "<a href=\"##{@lineanchorsid}#{number}\">#{number}</a>" - end - end - numbers.join("\n") - end - - def wrap_lines(lines) - if @lineanchors - lines = lines.each_with_index.map do |line, index| - number = index + @linenostart - - if @linenos == 'inline' - "<a name=\"L#{number}\"></a>" \ - "<span class=\"linenos\">#{number}</span>" \ - "<span id=\"#{@lineanchorsid}#{number}\" class=\"line\">#{line}" \ - '</span>' - else - "<span id=\"#{@lineanchorsid}#{number}\" class=\"line\">#{line}" \ - '</span>' - end - end - elsif @linenos == 'inline' - lines = lines.each_with_index.map do |line, index| - number = index + @linenostart - "<span class=\"linenos\">#{number}</span>#{line}" - end - end - - lines.join("\n") - end + def stream(tokens, &b) + is_first = true + token_lines(tokens) do |line| + yield "\n" unless is_first + is_first = false - def span(tok, val) - # http://stackoverflow.com/a/1600584/2587286 - val = CGI.escapeHTML(val) + yield %(<span id="LC#{@line_number}" class="line">) + line.each { |token, value| yield span(token, value) } + yield %(</span>) - if tok.shortname.empty? - val - else - if @inline_theme - rules = @inline_theme.style_for(tok).rendered_rules - "<span style=\"#{rules.to_a.join(';')}\"#{val}</span>" - else - "<span class=\"#{tok.shortname}\">#{val}</span>" - end + @line_number += 1 end end end diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index d521de28e8a..4a4892a2e07 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -49,7 +49,12 @@ server { proxy_http_version 1.1; - proxy_set_header Host $http_host; + ## By overwriting Host and clearing X-Forwarded-Host we ensure that + ## internal HTTP redirects generated by GitLab always send users to + ## YOUR_SERVER_FQDN. + proxy_set_header Host YOUR_SERVER_FQDN; + proxy_set_header X-Forwarded-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; diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index bf014b56cf6..0b93d7f292f 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -93,7 +93,12 @@ server { proxy_http_version 1.1; - proxy_set_header Host $http_host; + ## By overwriting Host and clearing X-Forwarded-Host we ensure that + ## internal HTTP redirects generated by GitLab always send users to + ## YOUR_SERVER_FQDN. + proxy_set_header Host YOUR_SERVER_FQDN; + proxy_set_header X-Forwarded-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; diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb deleted file mode 100644 index a3a3309e15e..00000000000 --- a/spec/controllers/commit_controller_spec.rb +++ /dev/null @@ -1,246 +0,0 @@ -require 'spec_helper' - -describe Projects::CommitController do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:commit) { project.commit("master") } - let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } - let(:master_pickable_commit) { project.commit(master_pickable_sha) } - - before do - sign_in(user) - project.team << [user, :master] - end - - describe "#show" do - shared_examples "export as" do |format| - it "should generally work" do - 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 - expect_any_instance_of(Commit).to receive(:"to_#{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, - 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 - allow_any_instance_of(Commit).to receive(:"to_#{format}"). - and_return('HTML entities &<>" ') - - get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: commit.id, 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 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: commit.id, - format: format) - - expect(response.body).to start_with("diff --git") - end - - it "should really only be a git diff without whitespace changes" do - get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: '66eceea0db202bb39c4e445e8ca28689645366c5', - # id: commit.id, - format: format, - w: 1) - - expect(response.body).to start_with("diff --git") - # without whitespace option, there are more than 2 diff_splits - diff_splits = assigns(:diffs).first.diff.split("\n") - expect(diff_splits.length).to be <= 2 - end - end - - describe "as patch" do - include_examples "export as", :patch - let(:format) { :patch } - - it "should really be a git email patch" do - 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, - 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 - - context 'commit that removes a submodule' do - render_views - - let(:fork_project) { create(:forked_project_with_submodules) } - let(:commit) { fork_project.commit('remove-submodule') } - - before do - fork_project.team << [user, :master] - end - - it 'renders it' do - get(:show, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project.to_param, - id: commit.id) - - expect(response).to be_success - end - end - end - - describe "#branches" do - 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 - - describe '#revert' do - context 'when target branch is not provided' do - it 'should render the 404 page' do - post(:revert, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: commit.id) - - expect(response).not_to be_success - expect(response).to have_http_status(404) - end - end - - context 'when the revert was successful' do - it 'should redirect to the commits page' do - post(:revert, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', - id: commit.id) - - expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') - expect(flash[:notice]).to eq('The commit has been successfully reverted.') - end - end - - context 'when the revert failed' do - before do - post(:revert, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', - id: commit.id) - end - - it 'should redirect to the commit page' do - # Reverting a commit that has been already reverted. - post(:revert, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', - id: commit.id) - - expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id) - expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.') - end - end - end - - describe '#cherry_pick' do - context 'when target branch is not provided' do - it 'should render the 404 page' do - post(:cherry_pick, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: master_pickable_commit.id) - - expect(response).not_to be_success - expect(response).to have_http_status(404) - end - end - - context 'when the cherry-pick was successful' do - it 'should redirect to the commits page' do - post(:cherry_pick, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', - id: master_pickable_commit.id) - - expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') - expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.') - end - end - - context 'when the cherry_pick failed' do - before do - post(:cherry_pick, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', - id: master_pickable_commit.id) - end - - it 'should redirect to the commit page' do - # Cherry-picking a commit that has been already cherry-picked. - post(:cherry_pick, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', - id: master_pickable_commit.id) - - expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) - expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.') - end - end - end -end diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 1f7fd517342..267d511c2db 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -11,7 +11,7 @@ describe HelpController do context 'for Markdown formats' do context 'when requested file exists' do before do - get :show, category: 'ssh', file: 'README', format: :md + get :show, path: 'ssh/README', format: :md end it 'assigns to @markdown' do @@ -26,7 +26,7 @@ describe HelpController do context 'when requested file is missing' do it 'renders not found' do - get :show, category: 'foo', file: 'bar', format: :md + get :show, path: 'foo/bar', format: :md expect(response).to be_not_found end end @@ -36,8 +36,7 @@ describe HelpController do context 'when requested file exists' do it 'renders the raw file' do get :show, - category: 'workflow/protected_branches', - file: 'protected_branches1', + path: 'workflow/protected_branches/protected_branches1', format: :png expect(response).to be_success expect(response.content_type).to eq 'image/png' @@ -48,8 +47,7 @@ describe HelpController do context 'when requested file is missing' do it 'renders not found' do get :show, - category: 'foo', - file: 'bar', + path: 'foo/bar', format: :png expect(response).to be_not_found end @@ -59,8 +57,7 @@ describe HelpController do context 'for other formats' do it 'always renders not found' do get :show, - category: 'ssh', - file: 'README', + path: 'ssh/README', format: :foo expect(response).to be_not_found end diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 6e3db10e451..3001d32e719 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -1,9 +1,29 @@ -require 'rails_helper' +require 'spec_helper' describe Projects::CommitController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:commit) { project.commit("master") } + let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } + let(:master_pickable_commit) { project.commit(master_pickable_sha) } + + before do + sign_in(user) + project.team << [user, :master] + end + describe 'GET show' do render_views + def go(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project.to_param + } + + get :show, params.merge(extra_params) + end + let(:project) { create(:project) } before do @@ -15,7 +35,7 @@ describe Projects::CommitController do context 'with valid id' do it 'responds with 200' do - go id: project.commit.id + go(id: commit.id) expect(response).to be_ok end @@ -23,27 +43,274 @@ describe Projects::CommitController do context 'with invalid id' do it 'responds with 404' do - go id: project.commit.id.reverse + go(id: commit.id.reverse) expect(response).to be_not_found end end it 'handles binary files' do - get(:show, + go(id: TestEnv::BRANCH_SHA['binary-encoding'], format: 'html') + + expect(response).to be_success + end + + shared_examples "export as" do |format| + it "should generally work" do + go(id: commit.id, format: format) + + expect(response).to be_success + end + + it "should generate it" do + expect_any_instance_of(Commit).to receive(:"to_#{format}") + + go(id: commit.id, format: format) + end + + it "should render it" do + go(id: commit.id, format: format) + + expect(response.body).to eq(commit.send(:"to_#{format}")) + end + + it "should not escape Html" do + allow_any_instance_of(Commit).to receive(:"to_#{format}"). + and_return('HTML entities &<>" ') + + go(id: commit.id, 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 as", :diff + let(:format) { :diff } + + it "should really only be a git diff" do + go(id: commit.id, format: format) + + expect(response.body).to start_with("diff --git") + end + + it "should really only be a git diff without whitespace changes" do + go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1) + + expect(response.body).to start_with("diff --git") + # without whitespace option, there are more than 2 diff_splits + diff_splits = assigns(:diffs).first.diff.split("\n") + expect(diff_splits.length).to be <= 2 + end + end + + describe "as patch" do + include_examples "export as", :patch + let(:format) { :patch } + + it "should really be a git email patch" do + go(id: commit.id, format: format) + + expect(response.body).to start_with("From #{commit.id}") + end + + it "should contain a git diff" do + go(id: commit.id, format: format) + + expect(response.body).to match(/^diff --git/) + end + end + + context 'commit that removes a submodule' do + render_views + + let(:fork_project) { create(:forked_project_with_submodules, visibility_level: 20) } + let(:commit) { fork_project.commit('remove-submodule') } + + it 'renders it' do + get(:show, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project.to_param, + id: commit.id) + + expect(response).to be_success + end + end + end + + describe "GET branches" do + it "contains branch and tags information" do + get(:branches, namespace_id: project.namespace.to_param, project_id: project.to_param, - id: TestEnv::BRANCH_SHA['binary-encoding'], - format: "html") + id: commit.id) - expect(response).to be_success + expect(assigns(:branches)).to include("master", "feature_conflict") + expect(assigns(:tags)).to include("v1.1.0") + end + end + + describe 'POST revert' do + context 'when target branch is not provided' do + it 'should render the 404 page' do + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: commit.id) + + expect(response).not_to be_success + expect(response).to have_http_status(404) + end + end + + context 'when the revert was successful' do + it 'should redirect to the commits page' do + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: commit.id) + + expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') + expect(flash[:notice]).to eq('The commit has been successfully reverted.') + end + end + + context 'when the revert failed' do + before do + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: commit.id) + end + + it 'should redirect to the commit page' do + # Reverting a commit that has been already reverted. + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: commit.id) + + expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id) + expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.') + end + end + end + + describe 'POST cherry_pick' do + context 'when target branch is not provided' do + it 'should render the 404 page' do + post(:cherry_pick, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: master_pickable_commit.id) + + expect(response).not_to be_success + expect(response).to have_http_status(404) + end + end + + context 'when the cherry-pick was successful' do + it 'should redirect to the commits page' do + post(:cherry_pick, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: master_pickable_commit.id) + + expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') + expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.') + end end - def go(id:) - get :show, + context 'when the cherry_pick failed' do + before do + post(:cherry_pick, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: master_pickable_commit.id) + end + + it 'should redirect to the commit page' do + # Cherry-picking a commit that has been already cherry-picked. + post(:cherry_pick, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: master_pickable_commit.id) + + expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) + expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.') + end + end + end + + describe 'GET diff_for_path' do + def diff_for_path(extra_params = {}) + params = { namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: id + project_id: project.to_param + } + + get :diff_for_path, params.merge(extra_params) + end + + let(:existing_path) { '.gitmodules' } + + context 'when the commit exists' do + context 'when the user has access to the project' do + context 'when the path exists in the diff' do + it 'enables diff notes' do + diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path) + + expect(assigns(:diff_notes_disabled)).to be_falsey + expect(assigns(:comments_target)).to eq(noteable_type: 'Commit', + commit_id: commit.id) + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| + expect(diffs.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs, diff_refs, project) + end + + diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path) + end + end + + context 'when the path does not exist in the diff' do + before { diff_for_path(id: commit.id, old_path: existing_path.succ, new_path: existing_path.succ) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the user does not have access to the project' do + before do + project.team.truncate + diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the commit does not exist' do + before { diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end end end end diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 4018dac95a2..4058d5e2453 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -64,4 +64,73 @@ describe Projects::CompareController do expect(assigns(:commits)).to eq(nil) end end + + describe 'GET diff_for_path' do + def diff_for_path(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project.to_param + } + + get :diff_for_path, params.merge(extra_params) + end + + let(:existing_path) { 'files/ruby/feature.rb' } + + context 'when the from and to refs exist' do + context 'when the user has access to the project' do + context 'when the path exists in the diff' do + it 'disables diff notes' do + diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path) + + expect(assigns(:diff_notes_disabled)).to be_truthy + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| + expect(diffs.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs, diff_refs, project) + end + + diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path) + end + end + + context 'when the path does not exist in the diff' do + before { diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the user does not have access to the project' do + before do + project.team.truncate + diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the from ref does not exist' do + before { diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + + context 'when the to ref does not exist' do + before { diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c4b57e77804..210085e3b1a 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -10,7 +10,7 @@ describe Projects::MergeRequestsController do project.team << [user, :master] end - describe '#new' do + describe 'GET new' do context 'merge request that removes a submodule' do render_views @@ -34,7 +34,7 @@ describe Projects::MergeRequestsController do end end - describe "#show" do + describe "GET show" do shared_examples "export merge as" do |format| it "should generally work" do get(:show, @@ -108,7 +108,7 @@ describe Projects::MergeRequestsController do end end - describe 'GET #index' do + describe 'GET index' do def get_merge_requests get :index, namespace_id: project.namespace.to_param, @@ -140,7 +140,7 @@ describe Projects::MergeRequestsController do end end - describe 'PUT #update' do + describe 'PUT update' do context 'there is no source project' do let(:project) { create(:project) } let(:fork_project) { create(:forked_project_with_submodules) } @@ -168,7 +168,7 @@ describe Projects::MergeRequestsController do end end - describe 'POST #merge' do + describe 'POST merge' do let(:base_params) do { namespace_id: project.namespace.path, @@ -266,7 +266,7 @@ describe Projects::MergeRequestsController do end end - describe "DELETE #destroy" do + describe "DELETE destroy" do it "denies access to users unless they're admin or project owner" do delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid @@ -290,96 +290,210 @@ describe Projects::MergeRequestsController do 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 + def go(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: merge_request.iid + } + + get :diffs, params.merge(extra_params) end - context 'as html' do - it 'renders the diff template' do - go + context 'with default params' do + context 'as html' do + before { go(format: 'html') } - expect(response).to render_template('diffs') + it 'renders the diff template' do + expect(response).to render_template('diffs') + end end - end - context 'as json' do - it 'renders the diffs template to a string' do - go format: 'json' + context 'as json' do + before { go(format: 'json') } - expect(response).to render_template('projects/merge_requests/show/_diffs') - expect(JSON.parse(response.body)).to have_key('html') + it 'renders the diffs template to a string' do + expect(response).to render_template('projects/merge_requests/show/_diffs') + expect(JSON.parse(response.body)).to have_key('html') + end 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) } + context 'with forked projects with submodules' do + render_views - 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 + 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) } - it 'renders' do - go format: 'json' + 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 + go(format: 'json') + end - expect(response).to be_success - expect(response.body).to have_content('Subproject commit') + it 'renders' do + expect(response).to be_success + expect(response.body).to have_content('Subproject commit') + end end end - end - describe 'GET diffs with ignore_whitespace_change' do - def go(format: 'html') - get :diffs, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: merge_request.iid, - format: format, - w: 1 - end + context 'with ignore_whitespace_change' do + context 'as html' do + before { go(format: 'html', w: 1) } - context 'as html' do - it 'renders the diff template' do - go + it 'renders the diff template' do + expect(response).to render_template('diffs') + end + end + + context 'as json' do + before { go(format: 'json', w: 1) } - expect(response).to render_template('diffs') + it 'renders the diffs template to a string' do + expect(response).to render_template('projects/merge_requests/show/_diffs') + expect(JSON.parse(response.body)).to have_key('html') + end end end - context 'as json' do - it 'renders the diffs template to a string' do - go format: 'json' + context 'with view' do + before { go(view: 'parallel') } - expect(response).to render_template('projects/merge_requests/show/_diffs') - expect(JSON.parse(response.body)).to have_key('html') + it 'saves the preferred diff view in a cookie' do + expect(response.cookies['diff_view']).to eq('parallel') end end end - describe 'GET diffs with view' do - def go(extra_params = {}) + describe 'GET diff_for_path' do + def diff_for_path(extra_params = {}) params = { namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: merge_request.iid + project_id: project.to_param } - get :diffs, params.merge(extra_params) + get :diff_for_path, params.merge(extra_params) end - it 'saves the preferred diff view in a cookie' do - go view: 'parallel' + context 'when an ID param is passed' do + let(:existing_path) { 'files/ruby/popen.rb' } - expect(response.cookies['diff_view']).to eq('parallel') + context 'when the merge request exists' do + context 'when the user can view the merge request' do + context 'when the path exists in the diff' do + it 'enables diff notes' do + diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) + + expect(assigns(:diff_notes_disabled)).to be_falsey + expect(assigns(:comments_target)).to eq(noteable_type: 'MergeRequest', + noteable_id: merge_request.id) + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| + expect(diffs.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs, diff_refs, project) + end + + diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) + end + end + + context 'when the path does not exist in the diff' do + before { diff_for_path(id: merge_request.iid, old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the user cannot view the merge request' do + before do + project.team.truncate + diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the merge request does not exist' do + before { diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + + context 'when the merge request belongs to a different project' do + let(:other_project) { create(:empty_project) } + + before do + other_project.team << [user, :master] + diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project.to_param) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when source and target params are passed' do + let(:existing_path) { 'files/ruby/feature.rb' } + + context 'when both branches are in the same project' do + it 'disables diff notes' do + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) + + expect(assigns(:diff_notes_disabled)).to be_truthy + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| + expect(diffs.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs, diff_refs, project) + end + + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) + end + end + + context 'when the source branch is in a different project to the target' do + let(:other_project) { create(:project) } + + before { other_project.team << [user, :master] } + + context 'when the path exists in the diff' do + it 'disables diff notes' do + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) + + expect(assigns(:diff_notes_disabled)).to be_truthy + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| + expect(diffs.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs, diff_refs, project) + end + + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) + end + end + + context 'when the path does not exist in the diff' do + before { diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb', merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end end end diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb index 5a8bba28594..936320a3709 100644 --- a/spec/controllers/projects/todo_controller_spec.rb +++ b/spec/controllers/projects/todo_controller_spec.rb @@ -1,6 +1,8 @@ require('spec_helper') describe Projects::TodosController do + include ApiHelpers + let(:user) { create(:user) } let(:project) { create(:project) } let(:issue) { create(:issue, project: project) } @@ -8,43 +10,51 @@ describe Projects::TodosController do context 'Issues' do describe 'POST create' do + def go + post :create, + namespace_id: project.namespace.path, + project_id: project.path, + issuable_id: issue.id, + issuable_type: 'issue', + format: 'html' + end + context 'when authorized' do before do sign_in(user) project.team << [user, :developer] end - it 'should create todo for issue' do + it 'creates todo for issue' do expect do - post(:create, namespace_id: project.namespace.path, - project_id: project.path, - issuable_id: issue.id, - issuable_type: 'issue') + go end.to change { user.todos.count }.by(1) expect(response).to have_http_status(200) end + + it 'returns todo path and pending count' do + go + + expect(response).to have_http_status(200) + expect(json_response['count']).to eq 1 + expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/) + end end context 'when not authorized' do - it 'should not create todo for issue that user has no access to' do + it 'does not create todo for issue that user has no access to' do sign_in(user) expect do - post(:create, namespace_id: project.namespace.path, - project_id: project.path, - issuable_id: issue.id, - issuable_type: 'issue') + go end.to change { user.todos.count }.by(0) expect(response).to have_http_status(404) end - it 'should not create todo for issue when user not logged in' do + it 'does not create todo for issue when user not logged in' do expect do - post(:create, namespace_id: project.namespace.path, - project_id: project.path, - issuable_id: issue.id, - issuable_type: 'issue') + go end.to change { user.todos.count }.by(0) expect(response).to have_http_status(302) @@ -55,43 +65,51 @@ describe Projects::TodosController do context 'Merge Requests' do describe 'POST create' do + def go + post :create, + namespace_id: project.namespace.path, + project_id: project.path, + issuable_id: merge_request.id, + issuable_type: 'merge_request', + format: 'html' + end + context 'when authorized' do before do sign_in(user) project.team << [user, :developer] end - it 'should create todo for merge request' do + it 'creates todo for merge request' do expect do - post(:create, namespace_id: project.namespace.path, - project_id: project.path, - issuable_id: merge_request.id, - issuable_type: 'merge_request') + go end.to change { user.todos.count }.by(1) expect(response).to have_http_status(200) end + + it 'returns todo path and pending count' do + go + + expect(response).to have_http_status(200) + expect(json_response['count']).to eq 1 + expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/) + end end context 'when not authorized' do - it 'should not create todo for merge request user has no access to' do + it 'does not create todo for merge request user has no access to' do sign_in(user) expect do - post(:create, namespace_id: project.namespace.path, - project_id: project.path, - issuable_id: merge_request.id, - issuable_type: 'merge_request') + go end.to change { user.todos.count }.by(0) expect(response).to have_http_status(404) end - it 'should not create todo for merge request user has no access to' do + it 'does not create todo for merge request user has no access to' do expect do - post(:create, namespace_id: project.namespace.path, - project_id: project.path, - issuable_id: merge_request.id, - issuable_type: 'merge_request') + go end.to change { user.todos.count }.by(0) expect(response).to have_http_status(302) diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index fe05a0cfc00..5fb671df570 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -15,6 +15,11 @@ FactoryGirl.define do services: ["postgres"] } end + yaml_variables do + [ + { key: :DB_NAME, value: 'postgres', public: true } + ] + end pipeline factory: :ci_pipeline diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 7fc20cd5555..866e663f026 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -23,6 +23,10 @@ FactoryGirl.define do action { Todo::BUILD_FAILED } end + trait :approval_required do + action { Todo::APPROVAL_REQUIRED } + end + trait :done do state :done end diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index a6198389f04..e177059d959 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -36,12 +36,45 @@ describe 'Admin Builds' do end end + context 'Pending tab' do + context 'when have pending builds' do + it 'shows pending builds' do + build1 = create(:ci_build, pipeline: pipeline, status: :pending) + build2 = create(:ci_build, pipeline: pipeline, status: :running) + build3 = create(:ci_build, pipeline: pipeline, status: :success) + build4 = create(:ci_build, pipeline: pipeline, status: :failed) + + visit admin_builds_path(scope: :pending) + + expect(page).to have_selector('.nav-links li.active', text: 'Pending') + expect(page.find('.build-link')).to have_content(build1.id) + expect(page.find('.build-link')).not_to have_content(build2.id) + expect(page.find('.build-link')).not_to have_content(build3.id) + expect(page.find('.build-link')).not_to have_content(build4.id) + expect(page).to have_link 'Cancel all' + end + end + + context 'when have no builds pending' do + it 'shows a message' do + create(:ci_build, pipeline: pipeline, status: :success) + + visit admin_builds_path(scope: :pending) + + expect(page).to have_selector('.nav-links li.active', text: 'Pending') + expect(page).to have_content 'No builds to show' + expect(page).not_to have_link 'Cancel all' + end + end + end + context 'Running tab' do context 'when have running builds' do it 'shows running builds' do - build1 = create(:ci_build, pipeline: pipeline, status: :pending) + build1 = create(:ci_build, pipeline: pipeline, status: :running) build2 = create(:ci_build, pipeline: pipeline, status: :success) build3 = create(:ci_build, pipeline: pipeline, status: :failed) + build4 = create(:ci_build, pipeline: pipeline, status: :pending) visit admin_builds_path(scope: :running) @@ -49,6 +82,7 @@ describe 'Admin Builds' do expect(page.find('.build-link')).to have_content(build1.id) expect(page.find('.build-link')).not_to have_content(build2.id) expect(page.find('.build-link')).not_to have_content(build3.id) + expect(page.find('.build-link')).not_to have_content(build4.id) expect(page).to have_link 'Cancel all' end end diff --git a/spec/features/compare_spec.rb b/spec/features/compare_spec.rb new file mode 100644 index 00000000000..c62556948e0 --- /dev/null +++ b/spec/features/compare_spec.rb @@ -0,0 +1,42 @@ +require "spec_helper" + +describe "Compare", js: true do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.team << [user, :master] + login_as user + visit namespace_project_compare_index_path(project.namespace, project, from: "master", to: "master") + end + + describe "branches" do + it "should pre-populate fields" do + expect(page.find_field("from").value).to eq("master") + end + + it "should compare branches" do + fill_in "from", with: "fea" + find("#from").click + + click_link "feature" + expect(page.find_field("from").value).to eq("feature") + + click_button "Compare" + expect(page).to have_content "Commits" + end + end + + describe "tags" do + it "should compare tags" do + fill_in "from", with: "v1.0" + find("#from").click + + click_link "v1.0.0" + expect(page.find_field("from").value).to eq("v1.0.0") + + click_button "Compare" + expect(page).to have_content "Commits" + end + end +end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb new file mode 100644 index 00000000000..78bc888f2a6 --- /dev/null +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -0,0 +1,207 @@ +require 'spec_helper' + +feature 'Expand and collapse diffs', js: true, feature: true do + include WaitForAjax + + before do + login_as :admin + project = create(:project) + branch = 'expand-collapse-diffs' + + # Ensure that undiffable.md is in .gitattributes + project.repository.copy_gitattributes(branch) + visit namespace_project_commit_path(project.namespace, project, project.commit(branch)) + execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });') + end + + def file_container(filename) + find("[data-blob-diff-path*='#{filename}']") + end + + # Use define_method instead of let (which is memoized) so that this just works across a + # reload. + # + files = [ + 'small_diff.md', 'large_diff.md', 'large_diff_renamed.md', 'undiffable.md', + 'too_large.md', 'too_large_image.jpg' + ] + + files.each do |file| + define_method(file.split('.').first) { file_container(file) } + end + + context 'visiting a commit with collapsed diffs' do + it 'shows small diffs immediately' do + expect(small_diff).to have_selector('.code') + expect(small_diff).not_to have_selector('.nothing-here-block') + end + + it 'collapses large diffs by default' do + expect(large_diff).not_to have_selector('.code') + expect(large_diff).to have_selector('.nothing-here-block') + end + + it 'collapses large diffs for renamed files by default' do + expect(large_diff_renamed).not_to have_selector('.code') + expect(large_diff_renamed).to have_selector('.nothing-here-block') + expect(large_diff_renamed).to have_selector('.file-title .deletion') + expect(large_diff_renamed).to have_selector('.file-title .addition') + end + + it 'shows non-renderable diffs as such immediately, regardless of their size' do + expect(undiffable).not_to have_selector('.code') + expect(undiffable).to have_selector('.nothing-here-block') + expect(undiffable).to have_content('gitattributes') + end + + it 'does not allow diffs that are larger than the maximum size to be expanded' do + expect(too_large).not_to have_selector('.code') + expect(too_large).to have_selector('.nothing-here-block') + expect(too_large).to have_content('too large') + end + + it 'shows image diffs immediately, regardless of their size' do + expect(too_large_image).not_to have_selector('.nothing-here-block') + expect(too_large_image).to have_selector('.image') + end + + context 'expanding a diff for a renamed file' do + before do + large_diff_renamed.find('.nothing-here-block').click + wait_for_ajax + end + + it 'shows the old content' do + old_line = large_diff_renamed.find('.line_content.old') + + expect(old_line).to have_content('two copies') + end + + it 'shows the new content' do + new_line = large_diff_renamed.find('.line_content.new', match: :prefer_exact) + + expect(new_line).to have_content('three copies') + end + end + + context 'expanding a large diff' do + before do + click_link('large_diff.md') + wait_for_ajax + end + + it 'makes a request to get the content' do + ajax_uris = evaluate_script('ajaxUris') + + expect(ajax_uris).not_to be_empty + expect(ajax_uris.first).to include('large_diff.md') + end + + it 'shows the diff content' do + expect(large_diff).to have_selector('.code') + expect(large_diff).not_to have_selector('.nothing-here-block') + end + + context 'adding a comment to the expanded diff' do + let(:comment_text) { 'A comment' } + + before do + large_diff.find('.diff-line-num', match: :prefer_exact).hover + large_diff.find('.add-diff-note').click + large_diff.find('.note-textarea').send_keys comment_text + large_diff.find_button('Comment').click + wait_for_ajax + end + + it 'adds the comment' do + expect(large_diff.find('.notes')).to have_content comment_text + end + + context 'reloading the page' do + before { refresh } + + it 'collapses the large diff by default' do + expect(large_diff).not_to have_selector('.code') + expect(large_diff).to have_selector('.nothing-here-block') + end + + context 'expanding the diff' do + before do + click_link('large_diff.md') + wait_for_ajax + end + + it 'shows the diff content' do + expect(large_diff).to have_selector('.code') + expect(large_diff).not_to have_selector('.nothing-here-block') + end + + it 'shows the diff comment' do + expect(large_diff.find('.notes')).to have_content comment_text + end + end + end + end + end + + context 'collapsing an expanded diff' do + before { click_link('small_diff.md') } + + it 'hides the diff content' do + expect(small_diff).not_to have_selector('.code') + expect(small_diff).to have_selector('.nothing-here-block') + end + + context 're-expanding the same diff' do + before { click_link('small_diff.md') } + + it 'shows the diff content' do + expect(small_diff).to have_selector('.code') + expect(small_diff).not_to have_selector('.nothing-here-block') + end + + it 'does not make a new HTTP request' do + expect(evaluate_script('ajaxUris')).not_to include(a_string_matching('small_diff.md')) + end + end + end + end + + context 'expanding all diffs' do + before do + click_link('Expand all') + wait_for_ajax + execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });') + end + + it 'reloads the page with all diffs expanded' do + expect(small_diff).to have_selector('.code') + expect(small_diff).not_to have_selector('.nothing-here-block') + + expect(large_diff).to have_selector('.code') + expect(large_diff).not_to have_selector('.nothing-here-block') + end + + context 'collapsing an expanded diff' do + before { click_link('small_diff.md') } + + it 'hides the diff content' do + expect(small_diff).not_to have_selector('.code') + expect(small_diff).to have_selector('.nothing-here-block') + end + + context 're-expanding the same diff' do + before { click_link('small_diff.md') } + + it 'shows the diff content' do + expect(small_diff).to have_selector('.code') + expect(small_diff).not_to have_selector('.nothing-here-block') + end + + it 'does not make a new HTTP request' do + expect(evaluate_script('ajaxUris')).not_to include(a_string_matching('small_diff.md')) + end + end + end + end +end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 891df65216d..2d8b59472e8 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -1,14 +1,26 @@ require 'spec_helper' feature 'Group', feature: true do + before do + login_as(:admin) + end + + describe 'creating a group with space in group path' do + it 'renders new group form with validation errors' do + visit new_group_path + fill_in 'Group path', with: 'space group' + + click_button 'Create group' + + expect(current_path).to eq(groups_path) + expect(page).to have_content("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.'.") + end + end + 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 diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 8c6b669ce78..1e2306d7f59 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -6,7 +6,7 @@ describe 'Help Pages', feature: true do login_as :user end it 'replace the variable $your_email with the email of the user' do - visit help_page_path('ssh', 'README') + visit help_page_path('ssh/README') expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"") end end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 72b5ff231f7..58753ff21f6 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -28,6 +28,11 @@ feature 'Login', feature: true do end describe 'with two-factor authentication' do + def enter_code(code) + fill_in 'Two-Factor Authentication code', with: code + click_button 'Verify code' + end + context 'with valid username/password' do let(:user) { create(:user, :two_factor) } @@ -36,11 +41,6 @@ feature 'Login', feature: true do 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.') @@ -108,6 +108,39 @@ feature 'Login', feature: true do end end end + + context 'logging in via OAuth' do + def saml_config + OpenStruct.new(name: 'saml', label: 'saml', args: { + assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback', + idp_cert_fingerprint: '26:43:2C:47:AF:F0:6B:D0:07:9C:AD:A3:74:FE:5D:94:5F:4E:9E:52', + idp_sso_target_url: 'https://idp.example.com/sso/saml', + issuer: 'https://localhost:3443/', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + }) + end + + def stub_omniauth_config(messages) + Rails.application.env_config['devise.mapping'] = Devise.mappings[:user] + Rails.application.routes.disable_clear_and_finalize = true + Rails.application.routes.draw do + post '/users/auth/saml' => 'omniauth_callbacks#saml' + end + allow(Gitlab::OAuth::Provider).to receive_messages(providers: [:saml], config_for: saml_config) + allow(Gitlab.config.omniauth).to receive_messages(messages) + allow_any_instance_of(Object).to receive(:user_omniauth_authorize_path).with('saml').and_return('/users/auth/saml') + end + + it 'should show 2FA prompt after OAuth login' do + stub_omniauth_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config]) + user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') + login_via('saml', user, 'my-uid') + + expect(page).to have_content('Two-Factor Authentication') + enter_code(user.current_otp) + expect(current_path).to eq root_path + end + end end describe 'without two-factor authentication' do diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb new file mode 100644 index 00000000000..c9a0059645d --- /dev/null +++ b/spec/features/merge_requests/diffs_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +feature 'Diffs URL', js: true, feature: true do + before do + login_as :admin + @merge_request = create(:merge_request) + @project = @merge_request.source_project + end + + context 'when visit with */* as accept header' do + before(:each) do + page.driver.add_header('Accept', '*/*') + end + + it 'renders the notes' do + create :note_on_merge_request, project: @project, noteable: @merge_request, note: 'Rebasing with master' + + visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + + # Load notes and diff through AJAX + expect(page).to have_css('.note-text', visible: false, text: 'Rebasing with master') + expect(page).to have_css('.diffs.tab-pane.active') + end + end +end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 5174168713c..0b38c413f44 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -135,6 +135,28 @@ describe 'Comments', feature: true do end end + describe 'Handles cross-project system notes', js: true, feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:project2) { create(:project, :private) } + let(:issue) { create(:issue, project: project2) } + let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'markdown') } + let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "mentioned in #{issue.to_reference(project)}") } + + it 'shows the system note' do + login_as :admin + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + + expect(page).to have_css('.system-note') + end + + it 'hides redacted system note' do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + + expect(page).not_to have_css('.system-note') + end + end + describe 'On a merge request diff', js: true, feature: true do let(:merge_request) { create(:merge_request) } let(:project) { merge_request.source_project } @@ -231,6 +253,7 @@ describe 'Comments', feature: true do end def click_diff_line(data = line_code) - execute_script("$('button[data-line-code=\"#{data}\"]').click()") + find(".line_holder[id='#{data}'] td.line_content").hover + find(".line_holder[id='#{data}'] button").trigger('click') end end diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index 16832c297ac..cab3dc1d167 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -13,17 +13,33 @@ describe "Builds" do end describe "GET /:project/builds" do + context "Pending scope" do + before do + visit namespace_project_builds_path(@project.namespace, @project, scope: :pending) + end + + it "shows Pending tab builds" do + expect(page).to have_link 'Cancel running' + expect(page).to have_selector('.nav-links li.active', text: 'Pending') + expect(page).to have_content @build.short_sha + expect(page).to have_content @build.ref + expect(page).to have_content @build.name + end + end + context "Running scope" do before do @build.run! visit namespace_project_builds_path(@project.namespace, @project, scope: :running) end - it { expect(page).to have_selector('.nav-links li.active', text: 'Running') } - it { expect(page).to have_link 'Cancel running' } - it { expect(page).to have_content @build.short_sha } - it { expect(page).to have_content @build.ref } - it { expect(page).to have_content @build.name } + it "shows Running tab builds" do + expect(page).to have_selector('.nav-links li.active', text: 'Running') + expect(page).to have_link 'Cancel running' + expect(page).to have_content @build.short_sha + expect(page).to have_content @build.ref + expect(page).to have_content @build.name + end end context "Finished scope" do @@ -32,9 +48,11 @@ describe "Builds" do visit namespace_project_builds_path(@project.namespace, @project, scope: :finished) end - it { expect(page).to have_selector('.nav-links li.active', text: 'Finished') } - it { expect(page).to have_content 'No builds to show' } - it { expect(page).to have_link 'Cancel running' } + it "shows Finished tab builds" do + expect(page).to have_selector('.nav-links li.active', text: 'Finished') + expect(page).to have_content 'No builds to show' + expect(page).to have_link 'Cancel running' + end end context "All builds" do @@ -43,11 +61,13 @@ describe "Builds" do visit namespace_project_builds_path(@project.namespace, @project) end - it { expect(page).to have_selector('.nav-links li.active', text: 'All') } - it { expect(page).to have_content @build.short_sha } - it { expect(page).to have_content @build.ref } - it { expect(page).to have_content @build.name } - it { expect(page).not_to have_link 'Cancel running' } + it "shows All tab builds" do + expect(page).to have_selector('.nav-links li.active', text: 'All') + expect(page).to have_content @build.short_sha + expect(page).to have_content @build.ref + expect(page).to have_content @build.name + expect(page).not_to have_link 'Cancel running' + end end end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index 6a39c302f55..98ba93b4036 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -76,7 +76,7 @@ feature 'Prioritize labels', feature: true do expect(page.all('li').last).to have_content('bug') end - visit current_url + refresh wait_for_ajax page.within('.prioritized-labels') do diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 13d980a326f..b6acc509342 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -426,4 +426,23 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } end + + describe "GET /:project_path/container_registry" do + before do + stub_container_registry_tags('latest') + stub_container_registry_config(enabled: true) + end + + subject { namespace_project_container_registry_index_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + 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 ac9690cc127..ccb5c06dab0 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -362,4 +362,23 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } end + + describe "GET /:project_path/container_registry" do + before do + stub_container_registry_tags('latest') + stub_container_registry_config(enabled: true) + end + + subject { namespace_project_container_registry_index_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + 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 737897de52b..985663e7c98 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -426,4 +426,23 @@ describe "Public Project Access", feature: true do it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } end + + describe "GET /:project_path/container_registry" do + before do + stub_container_registry_tags('latest') + stub_container_registry_config(enabled: true) + end + + subject { namespace_project_container_registry_index_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } + it { is_expected.to be_allowed_for :visitor } + end end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index 14613754f74..9335f5bf120 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do + before { allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) } + def register_u2f_device(u2f_device = nil) u2f_device ||= FakeU2fDevice.new(page) u2f_device.respond_to_u2f_registration @@ -208,21 +210,52 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: expect(page.body).to match('Authentication via U2F device failed') end end - end - describe "when two-factor authentication is disabled" do - let(:user) { create(:user) } + describe "when more than one device has been registered by the same user" do + it "allows logging in with either device" do + # Register first device + user = login_as(:user) + user.update_attribute(:otp_required_for_login, true) + visit profile_two_factor_auth_path + expect(page).to have_content("Your U2F device needs to be set up.") + first_device = register_u2f_device + + # Register second device + visit profile_two_factor_auth_path + expect(page).to have_content("Your U2F device needs to be set up.") + second_device = register_u2f_device + logout + + # Authenticate as both devices + [first_device, second_device].each do |device| + login_as(user) + device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" - before do - login_as(user) - user.update_attribute(:otp_required_for_login, true) - visit profile_account_path - click_on 'Manage Two-Factor Authentication' - register_u2f_device + expect(page.body).to match('Signed in successfully') + + logout + end + end end - it "deletes u2f registrations" do - expect { click_on "Disable" }.to change { U2fRegistration.count }.from(1).to(0) + describe "when two-factor authentication is disabled" do + let(:user) { create(:user) } + + before do + user = login_as(:user) + user.update_attribute(:otp_required_for_login, true) + visit profile_account_path + click_on 'Manage Two-Factor Authentication' + expect(page).to have_content("Your U2F device needs to be set up.") + register_u2f_device + end + + it "deletes u2f registrations" do + expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1) + end end end end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 1bd354815e4..8db897b1646 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -11,7 +11,7 @@ describe NotesFinder do project.team << [user, :master] end - describe :execute do + describe '#execute' do let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } before do diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml index 7d01183e3ef..37066c8e930 100644 --- a/spec/fixtures/parallel_diff_result.yml +++ b/spec/fixtures/parallel_diff_result.yml @@ -121,7 +121,7 @@ :type: old :number: 9 :text: | - -<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span> + -<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9 :position: !ruby/object:Gitlab::Diff::Position attributes: @@ -136,7 +136,7 @@ :type: new :number: 9 :text: | - +<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span> + +<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 :position: !ruby/object:Gitlab::Diff::Position attributes: @@ -241,7 +241,7 @@ :type: old :number: 13 :text: | - -<span id="LC13" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"PWD"</span> <span class="o">=></span> <span class="n">path</span> <span class="p">}</span></span> + -<span id="LC13" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"PWD"</span> <span class="o">=></span> <span class="n">path</span> <span class="p">}</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13 :position: !ruby/object:Gitlab::Diff::Position attributes: @@ -253,27 +253,6 @@ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: - :type: old - :number: - :text: '' - :line_code: - :position: -- :left: - :type: old - :number: 14 - :text: | - -<span id="LC14" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">chdir: </span><span class="n">path</span> <span class="p">}</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 14 - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: :type: new :number: 13 :text: | @@ -289,16 +268,17 @@ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 + :type: old + :number: 14 + :text: | + -<span id="LC14" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">chdir: </span><span class="n">path</span> <span class="p">}</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13 :position: !ruby/object:Gitlab::Diff::Position attributes: :old_path: files/ruby/popen.rb :new_path: files/ruby/popen.rb - :old_line: - :new_line: 14 + :old_line: 14 + :new_line: :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d @@ -335,7 +315,7 @@ :type: new :number: 15 :text: | - +<span id="LC15" class="line"> <span class="s2">"PWD"</span> <span class="o">=></span> <span class="n">path</span></span> + +<span id="LC15" class="line"> <span class="s2">"PWD"</span> <span class="o">=></span> <span class="n">path</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 :position: !ruby/object:Gitlab::Diff::Position attributes: @@ -643,7 +623,7 @@ :type: :number: 20 :text: |2 - <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">""</span></span> + <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">""</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 :position: !ruby/object:Gitlab::Diff::Position attributes: @@ -658,7 +638,7 @@ :type: :number: 26 :text: |2 - <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">""</span></span> + <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">""</span></span> :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 :position: !ruby/object:Gitlab::Diff::Position attributes: diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 6d1c02db297..bd0108f9938 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -16,19 +16,19 @@ describe BlobHelper do describe '#highlight' do it 'should return plaintext for unknown lexer context' do - result = helper.highlight(blob_name, no_context_content, nowrap: true) - expect(result).to eq('<span id="LC1" class="line">:type "assem"))</span>') + result = helper.highlight(blob_name, no_context_content) + expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line">:type "assem"))</span></code></pre>]) 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>] + expected = %Q[<pre class="code highlight"><code><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></code></pre>] - expect(helper.highlight(blob_name, blob_content, nowrap: true)).to eq(expected) + expect(helper.highlight(blob_name, blob_content)).to eq(expected) end it 'should highlight multi-line comments' do - result = helper.highlight(blob_name, multiline_content, nowrap: true) + result = helper.highlight(blob_name, multiline_content) html = Nokogiri::HTML(result) lines = html.search('.s') expect(lines.count).to eq(3) @@ -41,33 +41,19 @@ describe BlobHelper do let(:blob_name) { 'test.diff' } let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"} let(:expected) do - %q(<span id="LC1" class="line"><span class="gi">+aaa</span></span> + %q(<pre class="code highlight"><code><span id="LC1" class="line"><span class="gi">+aaa</span></span> <span id="LC2" class="line"><span class="gi">+bbb</span></span> <span id="LC3" class="line"><span class="gd">- ccc</span></span> -<span id="LC4" class="line"> ddd</span>) +<span id="LC4" class="line"> ddd</span></code></pre>) end it 'should highlight each line properly' do - result = helper.highlight(blob_name, blob_content, nowrap: true) + result = helper.highlight(blob_name, blob_content) expect(result).to eq(expected) end end end - describe "#highlighter" do - 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>' - ] - - highlighter = helper.highlighter(blob_name, blob_content, nowrap: true) - result = split_content.map{ |content| highlighter.highlight(content) } - expect(result).to eq(expected) - end - end - describe "#sanitize_svg" do let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') } let(:data) { open(input_svg_path).read } diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index e2db33d8345..4b134a48410 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -31,26 +31,11 @@ describe DiffHelper do end end - describe 'diff_hard_limit_enabled?' do - it 'should return true if param is provided' do - 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 - expect(diff_hard_limit_enabled?).to be_falsey - end - end - describe 'diff_options' do - it 'should return hard limit for a diff if force diff is true' do + it 'should return hard limit for a diff' do allow(controller).to receive(:params) { { force_show_diff: true } } expect(diff_options).to include(Commit.max_diff_options) end - - it 'should return safe limit for a diff if force diff is false' do - expect(diff_options).not_to include(:max_lines, :max_files) - end end describe 'unfold_bottom_class' do @@ -59,7 +44,7 @@ describe DiffHelper do end it 'should return js class when bottom lines should be unfolded' do - expect(unfold_bottom_class(true)).to eq('js-unfold-bottom') + expect(unfold_bottom_class(true)).to include('js-unfold-bottom') end end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index c0d2be98e85..6b5e3d93d48 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -57,7 +57,7 @@ describe EventsHelper do expected = '<pre class="code highlight js-syntax-highlight ruby">' \ "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \ " <span class=\"s1\">\'hello world\'</span>\n" \ - "<span class=\"k\">end</span>" \ + "<span class=\"k\">end</span>\n" \ '</code></pre>' expect(helper.event_note(input)).to eq(expected) end diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml index 470cabeafbb..06c2ab1e823 100644 --- a/spec/javascripts/fixtures/issues_show.html.haml +++ b/spec/javascripts/fixtures/issues_show.html.haml @@ -1,7 +1,7 @@ :css .hidden { display: none !important; } -.flash-container +.flash-container.flash-container-page .flash-alert .flash-notice diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee index f0d26fb5446..0244119fa0e 100644 --- a/spec/javascripts/project_title_spec.js.coffee +++ b/spec/javascripts/project_title_spec.js.coffee @@ -22,7 +22,7 @@ describe 'Project Title', -> @projects_data = fixture.load('projects.json')[0] spyOn(jQuery, 'ajax').and.callFake (req) => - expect(req.url).toBe('/api/v3/projects.json') + expect(req.url).toBe('/api/v3/projects.json?simple=true') d = $.Deferred() d.resolve @projects_data d.promise() diff --git a/spec/javascripts/u2f/authenticate_spec.coffee b/spec/javascripts/u2f/authenticate_spec.coffee index e8a2892d678..8ffeda11704 100644 --- a/spec/javascripts/u2f/authenticate_spec.coffee +++ b/spec/javascripts/u2f/authenticate_spec.coffee @@ -5,13 +5,12 @@ #= require ./mock_u2f_device describe 'U2FAuthenticate', -> - U2FUtil.enableTestMode() fixture.load('u2f/authenticate') beforeEach -> @u2fDevice = new MockU2FDevice @container = $("#js-authenticate-u2f") - @component = new U2FAuthenticate(@container, {}, "token") + @component = new U2FAuthenticate(@container, {sign_requests: []}, "token") @component.start() it 'allows authenticating via a U2F device', -> diff --git a/spec/javascripts/u2f/register_spec.js.coffee b/spec/javascripts/u2f/register_spec.js.coffee index 0858abeca1a..87dc769792b 100644 --- a/spec/javascripts/u2f/register_spec.js.coffee +++ b/spec/javascripts/u2f/register_spec.js.coffee @@ -5,7 +5,6 @@ #= require ./mock_u2f_device describe 'U2FRegister', -> - U2FUtil.enableTestMode() fixture.load('u2f/register') beforeEach -> diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 9e3d2f5825d..9276a154007 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -93,8 +93,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end it 'links with adjacent text' do - doc = reference_filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + doc = reference_filter("Label (#{reference}).") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\)\.)) end it 'ignores invalid label names' do @@ -104,8 +104,32 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end end + context 'String-based single-word references that begin with a digit' do + let(:label) { create(:label, name: '2fa', project: project) } + let(:reference) { "#{Label.reference_prefix}#{label.name}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See 2fa' + end + + it 'links with adjacent text' do + doc = reference_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.id}#{label.name.reverse}" + + expect(reference_filter(act).to_html).to eq exp + end + end + context 'String-based single-word references with special characters' do - let(:label) { create(:label, name: '?gfm&', project: project) } + let(:label) { create(:label, name: '?g.fm&', project: project) } let(:reference) { "#{Label.reference_prefix}#{label.name}" } it 'links to a valid reference' do @@ -113,17 +137,17 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do 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&' + expect(doc.text).to eq 'See ?g.fm&' end it 'links with adjacent text' do - doc = reference_filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(<a.+><span.+>\?gfm&</span></a>\.\))) + doc = reference_filter("Label (#{reference}).") + expect(doc.to_html).to match(%r(\(<a.+><span.+>\?g\.fm&</span></a>\)\.)) end it 'ignores invalid label names' do act = "Label #{Label.reference_prefix}#{label.name.reverse}" - exp = "Label #{Label.reference_prefix}&mfg?" + exp = "Label #{Label.reference_prefix}&mf.g?" expect(reference_filter(act).to_html).to eq exp end @@ -153,8 +177,32 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end end + context 'String-based multi-word references that begin with a digit' do + let(:label) { create(:label, name: '2 factor authentication', project: project) } + let(:reference) { label.to_reference(format: :name) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See 2 factor authentication' + end + + it 'links with adjacent text' do + doc = reference_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.id}#{label.name.reverse}" + + expect(reference_filter(act).to_html).to eq exp + end + end + context 'String-based multi-word references with special characters in quotes' do - let(:label) { create(:label, name: 'gfm & references?', project: project) } + let(:label) { create(:label, name: 'g.fm & references?', project: project) } let(:reference) { label.to_reference(format: :name) } it 'links to a valid reference' do @@ -162,22 +210,62 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do 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?' + expect(doc.text).to eq 'See g.fm & references?' end it 'links with adjacent text' do doc = reference_filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(<a.+><span.+>gfm & references\?</span></a>\.\))) + expect(doc.to_html).to match(%r(\(<a.+><span.+>g\.fm & references\?</span></a>\.\))) end it 'ignores invalid label names' do act = %(Label #{Label.reference_prefix}"#{label.name.reverse}") - exp = %(Label #{Label.reference_prefix}"?secnerefer & mfg\") + exp = %(Label #{Label.reference_prefix}"?secnerefer & mf.g\") expect(reference_filter(act).to_html).to eq exp end end + describe 'consecutive references' do + let(:bug) { create(:label, name: 'bug', project: project) } + let(:feature_proposal) { create(:label, name: 'feature proposal', project: project) } + let(:technical_debt) { create(:label, name: 'technical debt', project: project) } + + let(:bug_reference) { "#{Label.reference_prefix}#{bug.name}" } + let(:feature_proposal_reference) { feature_proposal.to_reference(format: :name) } + let(:technical_debt_reference) { technical_debt.to_reference(format: :name) } + + context 'separated with a comma' do + let(:references) { "#{bug_reference}, #{feature_proposal_reference}, #{technical_debt_reference}" } + + it 'links to valid references' do + doc = reference_filter("See #{references}") + + expect(doc.css('a').map { |a| a.attr('href') }).to match_array([ + urls.namespace_project_issues_url(project.namespace, project, label_name: bug.name), + urls.namespace_project_issues_url(project.namespace, project, label_name: feature_proposal.name), + urls.namespace_project_issues_url(project.namespace, project, label_name: technical_debt.name) + ]) + expect(doc.text).to eq 'See bug, feature proposal, technical debt' + end + end + + context 'separated with a space' do + let(:references) { "#{bug_reference} #{feature_proposal_reference} #{technical_debt_reference}" } + + it 'links to valid references' do + doc = reference_filter("See #{references}") + + expect(doc.css('a').map { |a| a.attr('href') }).to match_array([ + urls.namespace_project_issues_url(project.namespace, project, label_name: bug.name), + urls.namespace_project_issues_url(project.namespace, project, label_name: feature_proposal.name), + urls.namespace_project_issues_url(project.namespace, project, label_name: technical_debt.name) + ]) + expect(doc.text).to eq 'See bug feature proposal technical debt' + end + end + end + describe 'edge cases' do it 'gracefully handles non-references matching the pattern' do exp = act = '(format nil "~0f" 3.0) ; 3.0' diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb index 407617f3307..b1370bca833 100644 --- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb +++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb @@ -3,15 +3,35 @@ require 'spec_helper' describe Banzai::Filter::SyntaxHighlightFilter, lib: true do include FilterSpecHelper - it 'highlights valid code blocks' do - result = filter('<pre><code>def fun end</code>') - expect(result.to_html).to eq("<pre class=\"code highlight js-syntax-highlight plaintext\"><code>def fun end</code></pre>\n") + context "when no language is specified" do + it "highlights as plaintext" do + result = filter('<pre><code>def fun end</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext"><code>def fun end</code></pre>') + end end - it 'passes through invalid code blocks' do - allow_any_instance_of(described_class).to receive(:block_code).and_raise(StandardError) + context "when a valid language is specified" do + it "highlights as that language" do + result = filter('<pre><code class="ruby">def fun end</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>') + end + end + + context "when an invalid language is specified" do + it "highlights as plaintext" do + result = filter('<pre><code class="gnuplot">This is a test</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext"><code>This is a test</code></pre>') + end + end + + context "when Rouge formatting fails" do + before do + allow_any_instance_of(Rouge::Formatter).to receive(:format).and_raise(StandardError) + end - result = filter('<pre><code>This is a test</code></pre>') - expect(result.to_html).to eq('<pre>This is a test</pre>') + it "highlights as plaintext" do + result = filter('<pre><code class="ruby">This is a test</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight"><code>This is a test</code></pre>') + end end end diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb index 44256b32bdc..bcdb95250ca 100644 --- a/spec/lib/banzai/object_renderer_spec.rb +++ b/spec/lib/banzai/object_renderer_spec.rb @@ -17,6 +17,7 @@ describe Banzai::ObjectRenderer do and_call_original expect(object).to receive(:note_html=).with('<p>hello</p>') + expect(object).to receive(:user_visible_reference_count=).with(0) renderer.render([object], :note) end @@ -25,9 +26,10 @@ describe Banzai::ObjectRenderer do describe '#render_objects' do it 'renders an Array of objects' do object = double(:object, note: 'hello') + renderer = described_class.new(project, user) - expect(renderer).to receive(:render_attribute).with(object, :note). + expect(renderer).to receive(:render_attributes).with([object], :note). and_call_original rendered = renderer.render_objects([object], :note) @@ -38,7 +40,7 @@ describe Banzai::ObjectRenderer do end describe '#redact_documents' do - it 'redacts a set of documents and returns them as an Array of Strings' do + it 'redacts a set of documents and returns them as an Array of Hashes' do doc = Nokogiri::HTML.fragment('<p>hello</p>') renderer = described_class.new(project, user) @@ -48,7 +50,9 @@ describe Banzai::ObjectRenderer do redacted = renderer.redact_documents([doc]) - expect(redacted).to eq(['<p>hello</p>']) + expect(redacted.count).to eq(1) + expect(redacted.first[:visible_reference_count]).to eq(0) + expect(redacted.first[:document].to_html).to eq('<p>hello</p>') end end @@ -85,14 +89,36 @@ describe Banzai::ObjectRenderer do end end - describe '#render_attribute' do - it 'renders the attribute of an object' do - object = double(:doc, note: 'hello') + describe '#render_attributes' do + it 'renders the attribute of a list of objects' do + objects = [double(:doc, note: 'hello'), double(:doc, note: 'bye')] renderer = described_class.new(project, user, pipeline: :note) - doc = renderer.render_attribute(object, :note) - expect(doc).to be_an_instance_of(Nokogiri::HTML::DocumentFragment) - expect(doc.to_html).to eq('<p>hello</p>') + expect(Banzai).to receive(:cache_collection_render). + with([ + { text: 'hello', context: renderer.context_for(objects[0], :note) }, + { text: 'bye', context: renderer.context_for(objects[1], :note) } + ]). + and_call_original + + docs = renderer.render_attributes(objects, :note) + + expect(docs[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment) + expect(docs[0].to_html).to eq('<p>hello</p>') + + expect(docs[1]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment) + expect(docs[1].to_html).to eq('<p>bye</p>') + end + + it 'returns when no objects to render' do + objects = [] + renderer = described_class.new(project, user, pipeline: :note) + + expect(Banzai).to receive(:cache_collection_render). + with([]). + and_call_original + + expect(renderer.render_attributes(objects, :note)).to eq([]) end end diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb index 488f465bcda..254657a881d 100644 --- a/spec/lib/banzai/redactor_spec.rb +++ b/spec/lib/banzai/redactor_spec.rb @@ -15,11 +15,31 @@ describe Banzai::Redactor do expect(redactor).to receive(:nodes_visible_to_user).and_return([]) - expect(redactor.redact([doc1, doc2])).to eq([doc1, doc2]) + redacted_data = redactor.redact([doc1, doc2]) + expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2]) + expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0]) expect(doc1.to_html).to eq('foo') expect(doc2.to_html).to eq('bar') end + + it 'does not redact an Array of documents' do + doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>' + doc1 = Nokogiri::HTML.fragment(doc1_html) + + doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>' + doc2 = Nokogiri::HTML.fragment(doc2_html) + + nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] } + expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten) + + redacted_data = redactor.redact([doc1, doc2]) + + expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2]) + expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1]) + expect(doc1.to_html).to eq(doc1_html) + expect(doc2.to_html).to eq(doc2_html) + end end describe '#redact_nodes' do @@ -31,7 +51,7 @@ describe Banzai::Redactor do with([node]). and_return(Set.new) - redactor.redact_nodes([node]) + redactor.redact_document_nodes([{ document: doc, nodes: [node] }]) expect(doc.to_html).to eq('foo') end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index bad439bc489..ad6587b4c25 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -19,19 +19,18 @@ module Ci expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({ stage: "test", stage_idx: 1, - except: nil, name: :rspec, - only: nil, commands: "pwd\nrspec", tag_list: [], options: {}, allow_failure: false, when: "on_success", environment: nil, + yaml_variables: [] }) end - describe :only do + describe 'only' do it "does not return builds if only has another branch" do config = YAML.dump({ before_script: ["pwd"], @@ -187,7 +186,7 @@ module Ci end end - describe :except do + describe 'except' do it "returns builds if except has another branch" do config = YAML.dump({ before_script: ["pwd"], @@ -432,11 +431,9 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - except: nil, stage: "test", stage_idx: 1, name: :rspec, - only: nil, commands: "pwd\nrspec", tag_list: [], options: { @@ -446,6 +443,7 @@ module Ci allow_failure: false, when: "on_success", environment: nil, + yaml_variables: [] }) end @@ -461,11 +459,9 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - except: nil, stage: "test", stage_idx: 1, name: :rspec, - only: nil, commands: "pwd\nrspec", tag_list: [], options: { @@ -475,101 +471,126 @@ module Ci allow_failure: false, when: "on_success", environment: nil, + yaml_variables: [] }) end end describe 'Variables' do - context 'when global variables are defined' do - it 'returns global variables' do - variables = { - VAR1: 'value1', - VAR2: 'value2', - } + let(:config_processor) { GitlabCiYamlProcessor.new(YAML.dump(config), path) } - config = YAML.dump({ + subject { config_processor.builds.first[:yaml_variables] } + + context 'when global variables are defined' do + let(:variables) do + { VAR1: 'value1', VAR2: 'value2' } + end + let(:config) do + { variables: variables, before_script: ['pwd'], rspec: { script: 'rspec' } - }) + } + end - config_processor = GitlabCiYamlProcessor.new(config, path) + it 'returns global variables' do + expect(subject).to contain_exactly( + { key: :VAR1, value: 'value1', public: true }, + { key: :VAR2, value: 'value2', public: true } + ) + end + end + + context 'when job and global variables are defined' do + let(:global_variables) do + { VAR1: 'global1', VAR3: 'global3' } + end + let(:job_variables) do + { VAR1: 'value1', VAR2: 'value2' } + end + let(:config) do + { + before_script: ['pwd'], + variables: global_variables, + rspec: { script: 'rspec', variables: job_variables } + } + end - expect(config_processor.global_variables).to eq(variables) + it 'returns all unique variables' do + expect(subject).to contain_exactly( + { key: :VAR3, value: 'global3', public: true }, + { key: :VAR1, value: 'value1', public: true }, + { key: :VAR2, value: 'value2', public: true } + ) end end context 'when job variables are defined' do - context 'when syntax is correct' do - it 'returns job variables' do - variables = { - KEY1: 'value1', - SOME_KEY_2: 'value2' - } + let(:config) do + { + before_script: ['pwd'], + rspec: { script: 'rspec', variables: variables } + } + end + + context 'when also global variables are defined' do - config = YAML.dump( - { before_script: ['pwd'], - rspec: { - variables: variables, - script: 'rspec' } - }) + end - config_processor = GitlabCiYamlProcessor.new(config, path) + context 'when syntax is correct' do + let(:variables) do + { VAR1: 'value1', VAR2: 'value2' } + end - expect(config_processor.job_variables(:rspec)).to eq variables + it 'returns job variables' do + expect(subject).to contain_exactly( + { key: :VAR1, value: 'value1', public: true }, + { key: :VAR2, value: 'value2', public: true } + ) end end context 'when syntax is incorrect' do context 'when variables defined but invalid' do - it 'raises error' do - variables = [:KEY1, 'value1', :KEY2, 'value2'] - - config = YAML.dump( - { before_script: ['pwd'], - rspec: { - variables: variables, - script: 'rspec' } - }) + let(:variables) do + [ :VAR1, 'value1', :VAR2, 'value2' ] + end - expect { GitlabCiYamlProcessor.new(config, path) } + it 'raises error' do + expect { subject } .to raise_error(GitlabCiYamlProcessor::ValidationError, - /job: variables should be a map/) + /job: variables should be a map/) end end context 'when variables key defined but value not specified' do - it 'returns empty array' do - config = YAML.dump( - { before_script: ['pwd'], - rspec: { - variables: nil, - script: 'rspec' } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) + let(:variables) do + nil + end + it 'returns empty array' do ## # When variables config is empty, we assume this is a valid # configuration, see issue #18775 # - expect(config_processor.job_variables(:rspec)) - .to be_an_instance_of(Array).and be_empty + expect(subject).to be_an_instance_of(Array) + expect(subject).to be_empty end end end end context 'when job variables are not defined' do - it 'returns empty array' do - config = YAML.dump({ + let(:config) do + { before_script: ['pwd'], rspec: { script: 'rspec' } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) + } + end - expect(config_processor.job_variables(:rspec)).to eq [] + it 'returns empty array' do + expect(subject).to be_an_instance_of(Array) + expect(subject).to be_empty end end end @@ -681,11 +702,9 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - except: nil, stage: "test", stage_idx: 1, name: :rspec, - only: nil, commands: "pwd\nrspec", tag_list: [], options: { @@ -701,6 +720,7 @@ module Ci when: "on_success", allow_failure: false, environment: nil, + yaml_variables: [] }) end @@ -819,17 +839,16 @@ module Ci it "doesn't create jobs that start with dot" do expect(subject.size).to eq(1) expect(subject.first).to eq({ - except: nil, stage: "test", stage_idx: 1, name: :normal_job, - only: nil, commands: "test", tag_list: [], options: {}, when: "on_success", allow_failure: false, environment: nil, + yaml_variables: [] }) end end @@ -865,30 +884,28 @@ module Ci it "is correctly supported for jobs" do expect(subject.size).to eq(2) expect(subject.first).to eq({ - except: nil, stage: "build", stage_idx: 0, name: :job1, - only: nil, commands: "execute-script-for-job", tag_list: [], options: {}, when: "on_success", allow_failure: false, environment: nil, + yaml_variables: [] }) expect(subject.second).to eq({ - except: nil, stage: "build", stage_idx: 0, name: :job2, - only: nil, commands: "execute-script-for-job", tag_list: [], options: {}, when: "on_success", allow_failure: false, environment: nil, + yaml_variables: [] }) end end diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb index 4d8cb787dde..bbacdc67ebd 100644 --- a/spec/lib/container_registry/blob_spec.rb +++ b/spec/lib/container_registry/blob_spec.rb @@ -9,8 +9,9 @@ describe ContainerRegistry::Blob do 'size' => 1000 } end + let(:token) { 'authorization-token' } - let(:registry) { ContainerRegistry::Registry.new('http://example.com') } + let(:registry) { ContainerRegistry::Registry.new('http://example.com', token: token) } let(:repository) { registry.repository('group/test') } let(:blob) { repository.blob(config) } @@ -58,4 +59,53 @@ describe ContainerRegistry::Blob do it { is_expected.to be_truthy } end + + context '#data' do + let(:data) { '{"key":"value"}' } + + subject { blob.data } + + context 'when locally stored' do + before do + stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345'). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: data) + end + + it { is_expected.to eq(data) } + end + + context 'when externally stored' do + before do + stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345'). + with(headers: { 'Authorization' => "bearer #{token}" }). + to_return( + status: 307, + headers: { 'Location' => location }) + end + + context 'for a valid address' do + let(:location) { 'http://external.com/blob/file' } + + before do + stub_request(:get, location). + with(headers: { 'Authorization' => nil }). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: data) + end + + it { is_expected.to eq(data) } + end + + context 'for invalid file' do + let(:location) { 'file:///etc/passwd' } + + it { expect{ subject }.to raise_error(ArgumentError, 'invalid address') } + end + end + end end diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb index c7324c2bf77..c5e31ae82b6 100644 --- a/spec/lib/container_registry/tag_spec.rb +++ b/spec/lib/container_registry/tag_spec.rb @@ -77,24 +77,47 @@ describe ContainerRegistry::Tag do end context 'config processing' do - before do - stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac'). - with(headers: { 'Accept' => 'application/octet-stream' }). - to_return( - status: 200, - body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')) - end + shared_examples 'a processable' do + context '#config' do + subject { tag.config } - context '#config' do - subject { tag.config } + it { is_expected.not_to be_nil } + end + + context '#created_at' do + subject { tag.created_at } - it { is_expected.not_to be_nil } + it { is_expected.not_to be_nil } + end end - context '#created_at' do - subject { tag.created_at } + context 'when locally stored' do + before do + stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac'). + with(headers: { 'Accept' => 'application/octet-stream' }). + to_return( + status: 200, + body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')) + end + + it_behaves_like 'a processable' + end - it { is_expected.not_to be_nil } + context 'when externally stored' do + before do + stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac'). + with(headers: { 'Accept' => 'application/octet-stream' }). + to_return( + status: 307, + headers: { 'Location' => 'http://external.com/blob/file' }) + + stub_request(:get, 'http://external.com/blob/file'). + to_return( + status: 200, + body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')) + end + + it_behaves_like 'a processable' end end end diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb index 760d66a1488..7543c29bcc4 100644 --- a/spec/lib/gitlab/bitbucket_import/client_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb @@ -54,12 +54,12 @@ describe Gitlab::BitbucketImport::Client, lib: true do context 'project import' do it 'calls .from_project with no errors' do project = create(:empty_project) + project.import_url = "ssh://git@bitbucket.org/test/test.git" project.create_or_update_import_data(credentials: { user: "git", password: nil, bb_session: { bitbucket_access_token: "test", bitbucket_access_token_secret: "test" } }) - project.import_url = "ssh://git@bitbucket.org/test/test.git" expect { described_class.from_project(project) }.not_to raise_error end diff --git a/spec/lib/gitlab/build_data_builder_spec.rb b/spec/lib/gitlab/build_data_builder_spec.rb index 38be9448794..23ae5cfacc4 100644 --- a/spec/lib/gitlab/build_data_builder_spec.rb +++ b/spec/lib/gitlab/build_data_builder_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Gitlab::BuildDataBuilder' do let(:build) { create(:ci_build) } - describe :build do + describe '.build' do let(:data) do Gitlab::BuildDataBuilder.build(build) end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 9096ad101b0..4ec3f19e03f 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -13,6 +13,10 @@ describe Gitlab::Database::MigrationHelpers, lib: true do context 'outside a transaction' do before do expect(model).to receive(:transaction_open?).and_return(false) + + unless Gitlab::Database.postgresql? + allow_any_instance_of(Gitlab::Database::MigrationHelpers).to receive(:disable_statement_timeout) + end end context 'using PostgreSQL' do diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 1cb513d5229..0460dcf4658 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -8,14 +8,14 @@ describe Gitlab::Diff::File, lib: true do let(:diff) { commit.diffs.first } let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } - describe :diff_lines do + describe '#diff_lines' do let(:diff_lines) { diff_file.diff_lines } 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 + describe '#mode_changed?' do it { expect(diff_file.mode_changed?).to be_falsey } end diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index fb5d50a5c68..88e4115c453 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -28,13 +28,13 @@ describe Gitlab::Diff::Highlight, lib: true do end it 'highlights and marks removed lines' do - code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n} + code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n} expect(subject[4].text).to eq(code) end it 'highlights and marks added lines' do - code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n} + code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n} expect(subject[5].text).to eq(code) end diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb index 95a993d26cf..8ca3f73509e 100644 --- a/spec/lib/gitlab/diff/inline_diff_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_spec.rb @@ -3,14 +3,19 @@ require 'spec_helper' describe Gitlab::Diff::InlineDiff, lib: true do describe '.for_lines' do let(:diff) do - <<eos - class Test -- def initialize(test = true) -+ def initialize(test = false) - @test = test - end - end -eos + <<-EOF.strip_heredoc + class Test + - def initialize(test = true) + + def initialize(test = false) + @test = test + - if true + - @foo = "bar" + + unless false + + @foo = "baz" + end + end + end + EOF end let(:subject) { described_class.for_lines(diff.lines) } @@ -20,8 +25,11 @@ eos expect(subject[1]).to eq([25..27]) expect(subject[2]).to eq([25..28]) expect(subject[3]).to be_nil - expect(subject[4]).to be_nil - expect(subject[5]).to be_nil + expect(subject[4]).to eq([5..10]) + expect(subject[5]).to eq([17..17]) + expect(subject[6]).to eq([5..15]) + expect(subject[7]).to eq([17..17]) + expect(subject[8]).to be_nil end end diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index cdff063a9ed..c3359627652 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::Diff::Parser, lib: true do let(:diff) { commit.diffs.first } let(:parser) { Gitlab::Diff::Parser.new } - describe :parse do + describe '#parse' do let(:diff) do <<eos --- a/files/ruby/popen.rb diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index 3b023a35446..613c47d55f1 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -61,4 +61,11 @@ describe Gitlab::GithubImport::Client, lib: true do expect(client.api.api_endpoint).to eq 'https://github.company.com/' end end + + it 'does not raise error when rate limit is disabled' do + stub_request(:get, /api.github.com/) + allow(client.api).to receive(:rate_limit!).and_raise(Octokit::NotFound) + + expect { client.issues }.not_to raise_error + end end diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb new file mode 100644 index 00000000000..d3f1deb3837 --- /dev/null +++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Gitlab::GitlabImport::Importer, lib: true do + include ImportSpecHelper + + describe '#execute' do + before do + stub_omniauth_provider('gitlab') + stub_request('issues', [ + { + 'id' => 2579857, + 'iid' => 3, + 'title' => 'Issue', + 'description' => 'Lorem ipsum', + 'state' => 'opened', + 'author' => { + 'id' => 283999, + 'name' => 'John Doe' + } + } + ]) + stub_request('issues/2579857/notes', []) + end + + it 'persists issues' do + project = create(:empty_project, import_source: 'asd/vim') + project.build_import_data(credentials: { password: 'password' }) + + subject = described_class.new(project) + subject.execute + + expected_attributes = { + iid: 3, + title: 'Issue', + description: "*Created by: John Doe*\n\nLorem ipsum", + state: 'opened', + author_id: project.creator_id + } + + expect(project.issues.first).to have_attributes(expected_attributes) + end + + def stub_request(path, body) + url = "https://gitlab.com/api/v3/projects/asd%2Fvim/#{path}?page=1&per_page=100" + + WebMock.stub_request(:get, url). + to_return( + headers: { 'Content-Type' => 'application/json' }, + body: body + ) + end + end +end diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb new file mode 100644 index 00000000000..d6409a29550 --- /dev/null +++ b/spec/lib/gitlab/import_export/import_export_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::ImportExport, services: true do + describe 'export filename' do + let(:project) { create(:project, :public, path: 'project-path') } + + it 'contains the project path' do + expect(described_class.export_filename(project: project)).to include(project.path) + end + + it 'contains the namespace path' do + expect(described_class.export_filename(project: project)).to include(project.namespace.path) + end + + it 'does not go over a certain length' do + project.path = 'a' * 100 + + expect(described_class.export_filename(project: project).length).to be < 70 + end + end +end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 0b30e8c9b04..4113d829c3c 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -26,6 +26,7 @@ "deleted_at": null, "due_date": null, "moved_to_id": null, + "test_ee_field": "test", "notes": [ { "id": 351, @@ -4208,7 +4209,18 @@ "name": "User 4" }, "events": [ - + { + "id": 529, + "target_type": "Note", + "target_id": 2521, + "title": "test levels", + "data": null, + "project_id": 4, + "created_at": "2016-07-07T14:35:12.128Z", + "updated_at": "2016-07-07T14:35:12.128Z", + "action": 6, + "author_id": 1 + } ] }, { diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index a72aaa44e82..877be300262 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -24,11 +24,35 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(Ci::Pipeline.first.notes).not_to be_empty end - it 'restores the correct event' do + it 'restores the correct event with symbolised data' do restored_project_json expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty end + + it 'preserves updated_at on issues' do + restored_project_json + + issue = Issue.where(description: 'Aliquam enim illo et possimus.').first + + expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC') + end + + context 'event at forth level of the tree' do + let(:event) { Event.where(title: 'test levels').first } + + before do + restored_project_json + end + + it 'restores the event' do + expect(event).not_to be_nil + end + + it 'event belongs to note, belongs to merge request, belongs to a project' do + expect(event.note.noteable.project).not_to be_nil + end + end end end end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index f5b66b8156f..acd5394382c 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::LDAP::Access, lib: true do let(:access) { Gitlab::LDAP::Access.new user } let(:user) { create(:omniauth_user) } - describe :allowed? do + describe '#allowed?' do subject { access.allowed? } context 'when the user cannot be found' do diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 03199a2523e..949f6e2b19a 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -25,7 +25,7 @@ describe Gitlab::LDAP::User, lib: true do OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info_upper_case) end - describe :changed? do + 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 diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb deleted file mode 100644 index 659facd6c19..00000000000 --- a/spec/lib/gitlab/lfs/lfs_router_spec.rb +++ /dev/null @@ -1,730 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Lfs::Router, lib: true do - let(:project) { create(:project) } - let(:public_project) { create(:project, :public) } - let(:forked_project) { fork_project(public_project, user) } - - let(:user) { create(:user) } - let(:user_two) { create(:user) } - let!(:lfs_object) { create(:lfs_object, :with_file) } - - let(:request) { Rack::Request.new(env) } - let(:env) do - { - 'rack.input' => '', - 'REQUEST_METHOD' => 'GET', - } - end - - let(:lfs_router_auth) { new_lfs_router(project, user: user) } - let(:lfs_router_ci_auth) { new_lfs_router(project, ci: true) } - let(:lfs_router_noauth) { new_lfs_router(project) } - let(:lfs_router_public_auth) { new_lfs_router(public_project, user: user) } - let(:lfs_router_public_ci_auth) { new_lfs_router(public_project, ci: true) } - let(:lfs_router_public_noauth) { new_lfs_router(public_project) } - let(:lfs_router_forked_noauth) { new_lfs_router(forked_project) } - let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user: user_two) } - let(:lfs_router_forked_ci_auth) { new_lfs_router(forked_project, ci: true) } - - let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" } - let(:sample_size) { 499013 } - let(:respond_with_deprecated) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} - let(:respond_with_disabled) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} - - describe 'when lfs is disabled' do - before do - allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) - env['REQUEST_METHOD'] = 'POST' - body = { - 'objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }, - { 'oid' => sample_oid, - 'size' => sample_size - } - ], - 'operation' => 'upload' - }.to_json - env['rack.input'] = StringIO.new(body) - env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch" - end - - it 'responds with 501' do - expect(lfs_router_auth.try_call).to match_array(respond_with_disabled) - end - end - - describe 'when fetching lfs object using deprecated API' do - before do - enable_lfs - env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}" - end - - it 'responds with 501' do - expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated) - end - end - - describe 'when fetching lfs object' do - before do - enable_lfs - env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8" - env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}" - end - - describe 'and request comes from gitlab-workhorse' do - context 'without user being authorized' do - it "responds with status 401" do - expect(lfs_router_noauth.try_call.first).to eq(401) - end - end - - context 'with required headers' do - before do - project.lfs_objects << lfs_object - env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile" - end - - context 'when user does not have project access' do - it "responds with status 403" do - expect(lfs_router_auth.try_call.first).to eq(403) - end - end - - context 'when user has project access' do - before do - project.team << [user, :master] - end - - it "responds with status 200" do - expect(lfs_router_auth.try_call.first).to eq(200) - end - - it "responds with the file location" do - expect(lfs_router_auth.try_call[1]['Content-Type']).to eq("application/octet-stream") - expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path) - end - end - - context 'when CI is authorized' do - it "responds with status 200" do - expect(lfs_router_ci_auth.try_call.first).to eq(200) - end - - it "responds with the file location" do - expect(lfs_router_ci_auth.try_call[1]['Content-Type']).to eq("application/octet-stream") - expect(lfs_router_ci_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path) - end - end - end - - context 'without required headers' do - it "responds with status 403" do - expect(lfs_router_auth.try_call.first).to eq(403) - end - end - end - end - - describe 'when handling lfs request using deprecated API' do - before do - enable_lfs - env['REQUEST_METHOD'] = 'POST' - env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects" - end - - it 'responds with 501' do - expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated) - end - end - - describe 'when handling lfs batch request' do - before do - enable_lfs - env['REQUEST_METHOD'] = 'POST' - env['PATH_INFO'] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch" - end - - describe 'download' do - before do - body = { 'operation' => 'download', - 'objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size - }] - }.to_json - env['rack.input'] = StringIO.new(body) - end - - shared_examples 'an authorized requests' do - context 'when downloading an lfs object that is assigned to our project' do - before do - project.lfs_objects << lfs_object - end - - it 'responds with status 200 and href to download' do - response = router.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) - - expect(response_body).to eq('objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size, - 'actions' => { - 'download' => { - 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", - 'header' => { 'Authorization' => auth } - } - } - }]) - end - end - - context 'when downloading an lfs object that is assigned to other project' do - before do - public_project.lfs_objects << lfs_object - end - - it 'responds with status 200 and error message' do - response = router.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) - - expect(response_body).to eq('objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size, - 'error' => { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", - } - }]) - end - end - - context 'when downloading a lfs object that does not exist' do - before do - body = { 'operation' => 'download', - 'objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }] - }.to_json - env['rack.input'] = StringIO.new(body) - end - - it "responds with status 200 and error message" do - response = router.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) - - expect(response_body).to eq('objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078, - 'error' => { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", - } - }]) - end - end - - context 'when downloading one new and one existing lfs object' do - before do - body = { 'operation' => 'download', - 'objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }, - { 'oid' => sample_oid, - 'size' => sample_size - } - ] - }.to_json - env['rack.input'] = StringIO.new(body) - project.lfs_objects << lfs_object - end - - it "responds with status 200 with upload hypermedia link for the new object" do - response = router.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) - - expect(response_body).to eq('objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078, - 'error' => { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", - } - }, - { 'oid' => sample_oid, - 'size' => sample_size, - 'actions' => { - 'download' => { - 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", - 'header' => { 'Authorization' => auth } - } - } - }]) - end - end - end - - context 'when user is authenticated' do - let(:auth) { authorize(user) } - - before do - env["HTTP_AUTHORIZATION"] = auth - project.team << [user, role] - end - - it_behaves_like 'an authorized requests' do - let(:role) { :reporter } - let(:router) { lfs_router_auth } - end - - context 'when user does is not member of the project' do - let(:role) { :guest } - - it 'responds with 403' do - expect(lfs_router_auth.try_call.first).to eq(403) - end - end - - context 'when user does not have download access' do - let(:role) { :guest } - - it 'responds with 403' do - expect(lfs_router_auth.try_call.first).to eq(403) - end - end - end - - context 'when CI is authorized' do - let(:auth) { 'gitlab-ci-token:password' } - - before do - env["HTTP_AUTHORIZATION"] = auth - end - - it_behaves_like 'an authorized requests' do - let(:router) { lfs_router_ci_auth } - end - end - - context 'when user is not authenticated' do - describe 'is accessing public project' do - before do - public_project.lfs_objects << lfs_object - end - - it 'responds with status 200 and href to download' do - response = lfs_router_public_noauth.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) - - expect(response_body).to eq('objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size, - 'actions' => { - 'download' => { - 'href' => "#{public_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", - 'header' => {} - } - } - }]) - end - end - - describe 'is accessing non-public project' do - before do - project.lfs_objects << lfs_object - end - - it 'responds with authorization required' do - expect(lfs_router_noauth.try_call.first).to eq(401) - end - end - end - end - - describe 'upload' do - before do - body = { 'operation' => 'upload', - 'objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size - }] - }.to_json - env['rack.input'] = StringIO.new(body) - end - - describe 'when request is authenticated' do - describe 'when user has project push access' do - before do - @auth = authorize(user) - env["HTTP_AUTHORIZATION"] = @auth - project.team << [user, :developer] - end - - context 'when pushing an lfs object that already exists' do - before do - public_project.lfs_objects << lfs_object - end - - it "responds with status 200 and links the object to the project" do - response_body = lfs_router_auth.try_call.last - response = ActiveSupport::JSON.decode(response_body.first) - - expect(response['objects']).to be_kind_of(Array) - expect(response['objects'].first['oid']).to eq(sample_oid) - expect(response['objects'].first['size']).to eq(sample_size) - expect(lfs_object.projects.pluck(:id)).not_to include(project.id) - expect(lfs_object.projects.pluck(:id)).to include(public_project.id) - expect(response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}") - expect(response['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth) - end - end - - context 'when pushing a lfs object that does not exist' do - before do - body = { 'operation' => 'upload', - 'objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }] - }.to_json - env['rack.input'] = StringIO.new(body) - end - - it "responds with status 200 and upload hypermedia link" do - response = lfs_router_auth.try_call - expect(response.first).to eq(200) - - response_body = ActiveSupport::JSON.decode(response.last.first) - expect(response_body['objects']).to be_kind_of(Array) - expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") - expect(response_body['objects'].first['size']).to eq(1575078) - expect(lfs_object.projects.pluck(:id)).not_to include(project.id) - expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") - expect(response_body['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth) - end - end - - context 'when pushing one new and one existing lfs object' do - before do - body = { 'operation' => 'upload', - 'objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }, - { 'oid' => sample_oid, - 'size' => sample_size - } - ] - }.to_json - env['rack.input'] = StringIO.new(body) - project.lfs_objects << lfs_object - end - - it "responds with status 200 with upload hypermedia link for the new object" do - response = lfs_router_auth.try_call - expect(response.first).to eq(200) - - response_body = ActiveSupport::JSON.decode(response.last.first) - expect(response_body['objects']).to be_kind_of(Array) - - expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") - expect(response_body['objects'].first['size']).to eq(1575078) - expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") - expect(response_body['objects'].first['actions']['upload']['header']).to eq("Authorization" => @auth) - - expect(response_body['objects'].last['oid']).to eq(sample_oid) - expect(response_body['objects'].last['size']).to eq(sample_size) - expect(response_body['objects'].last).not_to have_key('actions') - end - end - end - - context 'when user does not have push access' do - it 'responds with 403' do - expect(lfs_router_auth.try_call.first).to eq(403) - end - end - - context 'when CI is authorized' do - it 'responds with 401' do - expect(lfs_router_ci_auth.try_call.first).to eq(401) - end - end - end - - context 'when user is not authenticated' do - context 'when user has push access' do - before do - project.team << [user, :master] - end - - it "responds with status 401" do - expect(lfs_router_public_noauth.try_call.first).to eq(401) - end - end - - context 'when user does not have push access' do - it "responds with status 401" do - expect(lfs_router_public_noauth.try_call.first).to eq(401) - end - end - end - - context 'when CI is authorized' do - let(:auth) { 'gitlab-ci-token:password' } - - before do - env["HTTP_AUTHORIZATION"] = auth - end - - it "responds with status 403" do - expect(lfs_router_public_ci_auth.try_call.first).to eq(401) - end - end - end - - describe 'unsupported' do - before do - body = { 'operation' => 'other', - 'objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size - }] - }.to_json - env['rack.input'] = StringIO.new(body) - end - - it 'responds with status 404' do - expect(lfs_router_public_noauth.try_call.first).to eq(404) - end - end - end - - describe 'when pushing a lfs object' do - before do - enable_lfs - env['REQUEST_METHOD'] = 'PUT' - end - - shared_examples 'unauthorized' do - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(router.project) - end - - it 'responds with status 401' do - expect(router.try_call.first).to eq(401) - end - end - - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(router.project) - end - - it 'responds with status 401' do - expect(router.try_call.first).to eq(401) - end - end - - context 'and request is sent with a malformed headers' do - before do - env["PATH_INFO"] = "#{router.project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}" - env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd" - end - - it 'does not recognize it as a valid lfs command' do - expect(router.try_call).to eq(nil) - end - end - end - - shared_examples 'forbidden' do - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(router.project) - end - - it 'responds with 403' do - expect(router.try_call.first).to eq(403) - end - end - - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(router.project) - end - - it 'responds with 403' do - expect(router.try_call.first).to eq(403) - end - end - end - - describe 'to one project' do - describe 'when user is authenticated' do - describe 'when user has push access to the project' do - before do - project.team << [user, :developer] - end - - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(project) - end - - it 'responds with status 200, location of lfs store and object details' do - json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first) - - expect(lfs_router_auth.try_call.first).to eq(200) - expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload") - expect(json_response['LfsOid']).to eq(sample_oid) - expect(json_response['LfsSize']).to eq(sample_size) - end - end - - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(project) - end - - it 'responds with status 200 and lfs object is linked to the project' do - expect(lfs_router_auth.try_call.first).to eq(200) - expect(lfs_object.projects.pluck(:id)).to include(project.id) - end - end - end - - describe 'and user does not have push access' do - let(:router) { lfs_router_auth } - - it_behaves_like 'forbidden' - end - end - - context 'when CI is authenticated' do - let(:router) { lfs_router_ci_auth } - - it_behaves_like 'unauthorized' - end - - context 'for unauthenticated' do - let(:router) { new_lfs_router(project) } - - it_behaves_like 'unauthorized' - end - end - - describe 'to a forked project' do - let(:forked_project) { fork_project(public_project, user) } - - describe 'when user is authenticated' do - describe 'when user has push access to the project' do - before do - forked_project.team << [user_two, :developer] - end - - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(forked_project) - end - - it 'responds with status 200, location of lfs store and object details' do - json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first) - - expect(lfs_router_forked_auth.try_call.first).to eq(200) - expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload") - expect(json_response['LfsOid']).to eq(sample_oid) - expect(json_response['LfsSize']).to eq(sample_size) - end - end - - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(forked_project) - end - - it 'responds with status 200 and lfs object is linked to the source project' do - expect(lfs_router_forked_auth.try_call.first).to eq(200) - expect(lfs_object.projects.pluck(:id)).to include(public_project.id) - end - end - end - - describe 'and user does not have push access' do - let(:router) { lfs_router_forked_auth } - - it_behaves_like 'forbidden' - end - end - - context 'when CI is authenticated' do - let(:router) { lfs_router_forked_ci_auth } - - it_behaves_like 'unauthorized' - end - - context 'for unauthenticated' do - let(:router) { lfs_router_forked_noauth } - - it_behaves_like 'unauthorized' - end - - describe 'and second project not related to fork or a source project' do - let(:second_project) { create(:project) } - let(:lfs_router_second_project) { new_lfs_router(second_project, user: user) } - - before do - public_project.lfs_objects << lfs_object - headers_for_upload_finalize(second_project) - end - - context 'when pushing the same lfs object to the second project' do - before do - second_project.team << [user, :master] - end - - it 'responds with 200 and links the lfs object to the project' do - expect(lfs_router_second_project.try_call.first).to eq(200) - expect(lfs_object.projects.pluck(:id)).to include(second_project.id, public_project.id) - end - end - end - end - end - - def enable_lfs - allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) - end - - def authorize(user) - ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) - end - - def new_lfs_router(project, user: nil, ci: false) - Gitlab::Lfs::Router.new(project, user, ci, request) - end - - def header_for_upload_authorize(project) - env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize" - end - - def headers_for_upload_finalize(project) - env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}" - env["HTTP_X_GITLAB_LFS_TMP"] = "#{sample_oid}6e561c9d4" - end - - def fork_project(project, user, object = nil) - allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) - Projects::ForkService.new(project, user, {}).execute - end -end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 8a8a4b46f08..d435cd745b3 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -210,7 +210,7 @@ describe Ci::Build, models: true do end before do - build.update_attributes(stage: 'stage') + build.update_attributes(stage: 'stage', yaml_variables: yaml_variables) end it { is_expected.to eq(predefined_variables + yaml_variables) } @@ -262,22 +262,6 @@ describe Ci::Build, models: true do it { is_expected.to eq(predefined_variables + predefined_trigger_variable + yaml_variables + secure_variables + trigger_variables) } end - - context 'when job variables are defined' do - ## - # Job-level variables are defined in gitlab_ci.yml fixture - # - context 'when job variables are unique' do - let(:build) { create(:ci_build, name: 'staging') } - - it 'includes job variables' do - expect(subject).to include( - { key: :KEY1, value: 'value1', public: true }, - { key: :KEY2, value: 'value2', public: true } - ) - end - end - end end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 34507cf5083..10db79bd15f 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -5,9 +5,12 @@ describe Ci::Pipeline, models: true do let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:user) } + it { is_expected.to have_many(:statuses) } it { is_expected.to have_many(:trigger_requests) } it { is_expected.to have_many(:builds) } + it { is_expected.to validate_presence_of :sha } it { is_expected.to validate_presence_of :status } @@ -15,7 +18,7 @@ describe Ci::Pipeline, models: true do it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :short_sha } - describe :valid_commit_sha do + describe '#valid_commit_sha' do context 'commit.sha can not start with 00000000' do before do pipeline.sha = '0' * 40 @@ -26,7 +29,7 @@ describe Ci::Pipeline, models: true do end end - describe :short_sha do + describe '#short_sha' do subject { pipeline.short_sha } it 'has 8 items' do @@ -35,10 +38,10 @@ describe Ci::Pipeline, models: true do it { expect(pipeline.sha).to start_with(subject) } end - describe :create_next_builds do + describe '#create_next_builds' do end - describe :retried do + describe '#retried' do subject { pipeline.retried } before do @@ -51,7 +54,7 @@ describe Ci::Pipeline, models: true do end end - describe :create_builds do + describe '#create_builds' do let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project, ref: 'master', tag: false } def create_builds(trigger_request = nil) diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 98f60087cf5..4e7833c3162 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -9,7 +9,7 @@ describe Ci::Variable, models: true do subject.value = secret_value end - describe :value do + describe '#value' do it 'stores the encrypted value' do expect(subject.encrypted_value).not_to be_nil end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 96397d7c8a9..ff6371ad685 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -24,14 +24,14 @@ describe CommitStatus, models: true do it { is_expected.to respond_to :running? } it { is_expected.to respond_to :pending? } - describe :author do + describe '#author' do subject { commit_status.author } before { commit_status.author = User.new } it { is_expected.to eq(commit_status.user) } end - describe :started? do + describe '#started?' do subject { commit_status.started? } context 'without started_at' do @@ -57,7 +57,7 @@ describe CommitStatus, models: true do end end - describe :active? do + describe '#active?' do subject { commit_status.active? } %w(pending running).each do |state| @@ -77,7 +77,7 @@ describe CommitStatus, models: true do end end - describe :complete? do + describe '#complete?' do subject { commit_status.complete? } %w(success failed canceled).each do |state| @@ -97,7 +97,7 @@ describe CommitStatus, models: true do end end - describe :duration do + describe '#duration' do subject { commit_status.duration } it { is_expected.to eq(120.0) } @@ -122,7 +122,7 @@ describe CommitStatus, models: true do end end - describe :latest do + describe '.latest' do subject { CommitStatus.latest.order(:id) } before do @@ -138,7 +138,7 @@ describe CommitStatus, models: true do end end - describe :running_or_pending do + describe '.running_or_pending' do subject { CommitStatus.running_or_pending.order(:id) } before do @@ -177,10 +177,10 @@ describe CommitStatus, models: true do describe '#stages' do before do - FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'build', stage_idx: 0, status: 'success' - FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'build', stage_idx: 0, status: 'failed' - FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'deploy', stage_idx: 2, status: 'running' - FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'test', stage_idx: 1, status: 'success' + create :commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success' + create :commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed' + create :commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running' + create :commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success' end context 'stages list' do @@ -192,7 +192,7 @@ describe CommitStatus, models: true do end context 'stages with statuses' do - subject { CommitStatus.where(pipeline: pipeline).stages_status } + subject { CommitStatus.where(pipeline: pipeline).latest.stages_status } it 'return list of stages with statuses' do is_expected.to eq({ @@ -201,6 +201,20 @@ describe CommitStatus, models: true do 'deploy' => 'running' }) end + + context 'when build is retried' do + before do + create :commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success' + end + + it 'ignores a previous state' do + is_expected.to eq({ + 'build' => 'success', + 'test' => 'success', + 'deploy' => 'running' + }) + end + end end end diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index 0344dae8b5d..5e652660e2c 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -7,7 +7,7 @@ describe Mentionable do nil end - describe :references do + describe 'references' do let(:project) { create(:project) } it 'excludes JIRA references' do diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb index fa1a0d4e0c7..f94987dcaff 100644 --- a/spec/models/forked_project_link_spec.rb +++ b/spec/models/forked_project_link_spec.rb @@ -18,7 +18,7 @@ describe ForkedProjectLink, "add link on fork" do end end -describe :forked_from_project do +describe '#forked?' do let(:forked_project_link) { build(:forked_project_link) } let(:project_from) { create(:project) } let(:project_to) { create(:project, forked_project_link: forked_project_link) } diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index c4e781dd1dc..615cfe3142b 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -4,33 +4,33 @@ describe GenericCommitStatus, models: true do let(:pipeline) { FactoryGirl.create :ci_pipeline } let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline } - describe :context do + describe '#context' do subject { generic_commit_status.context } before { generic_commit_status.context = 'my_context' } it { is_expected.to eq(generic_commit_status.name) } end - describe :tags do + describe '#tags' do subject { generic_commit_status.tags } it { is_expected.to eq([:external]) } end - describe :set_default_values do + describe 'set_default_values' do before do generic_commit_status.context = nil generic_commit_status.stage = nil generic_commit_status.save end - describe :context do + describe '#context' do subject { generic_commit_status.context } it { is_expected.not_to be_nil } end - describe :stage do + describe '#stage' do subject { generic_commit_status.stage } it { is_expected.not_to be_nil } diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index 197c99cd007..ae77ec5b348 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -14,7 +14,7 @@ describe GlobalMilestone, models: true do let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) } let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) } - describe :build_collection do + describe '.build_collection' do before do milestones = [ @@ -42,7 +42,7 @@ describe GlobalMilestone, models: true do end end - describe :initialize do + describe '#initialize' do before do milestones = [ @@ -63,7 +63,7 @@ describe GlobalMilestone, models: true do end end - describe :safe_title do + describe '#safe_title' do let(:milestone) { create(:milestone, title: "git / test", project: project1) } it 'should strip out slashes and spaces' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index a878ff1b227..266c46213a6 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -97,22 +97,22 @@ describe Group, models: true do end end - describe :users do + describe '#users' do it { expect(group.users).to eq(group.owners) } end - describe :human_name do + describe '#human_name' do it { expect(group.human_name).to eq(group.name) } end - describe :add_users do + describe '#add_user' do let(:user) { create(:user) } before { group.add_user(user, GroupMember::MASTER) } it { expect(group.group_members.masters.map(&:user)).to include(user) } end - describe :add_users do + describe '#add_users' do let(:user) { create(:user) } before { group.add_users([user.id], GroupMember::GUEST) } @@ -124,7 +124,7 @@ describe Group, models: true do end end - describe :avatar_type do + describe '#avatar_type' do let(:user) { create(:user) } before { group.add_user(user, GroupMember::MASTER) } diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb index d64d89edbd3..d23fc06c3ad 100644 --- a/spec/models/legacy_diff_note_spec.rb +++ b/spec/models/legacy_diff_note_spec.rb @@ -16,10 +16,10 @@ describe LegacyDiffNote, models: true do end describe '#active?' do - it 'is always true when the note has no associated diff' do + it 'is always true when the note has no associated diff line' do note = build(:legacy_diff_note_on_merge_request) - expect(note).to receive(:diff).and_return(nil) + expect(note).to receive(:diff_line).and_return(nil) expect(note).to be_active end @@ -27,7 +27,7 @@ describe LegacyDiffNote, models: true do it 'is never true when the note has no noteable associated' do note = build(:legacy_diff_note_on_merge_request) - expect(note).to receive(:diff).and_return(double) + expect(note).to receive(:diff_line).and_return(double) expect(note).to receive(:noteable).and_return(nil) expect(note).not_to be_active @@ -47,7 +47,7 @@ describe LegacyDiffNote, models: true do merge = build_stubbed(:merge_request, :simple) note = build(:legacy_diff_note_on_merge_request, noteable: merge) - allow(note).to receive(:diff).and_return(double) + allow(note).to receive(:diff_line).and_return(double) expect(note).to receive(:find_noteable_diff).and_return(nil) expect(note).not_to be_active diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 4c103462433..ba622dfb9be 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -101,7 +101,7 @@ describe ProjectMember, models: true do end end - describe :add_users_into_projects do + describe '.add_users_into_projects' do before do @project_1 = create :project @project_2 = create :project @@ -123,7 +123,7 @@ describe ProjectMember, models: true do it { expect(@project_2.users).to include(@user_2) } end - describe :truncate_teams do + describe '.truncate_teams' do before do @project_1 = create :project @project_2 = create :project diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb new file mode 100644 index 00000000000..9a637c94fbe --- /dev/null +++ b/spec/models/merge_request_diff_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe MergeRequestDiff, models: true do + describe '#diffs' do + let(:mr) { create(:merge_request, :with_diffs) } + let(:mr_diff) { mr.merge_request_diff } + + context 'when the :ignore_whitespace_change option is set' do + it 'creates a new compare object instead of loading from the DB' do + expect(mr_diff).not_to receive(:load_diffs) + expect(Gitlab::Git::Compare).to receive(:new).and_call_original + + mr_diff.diffs(ignore_whitespace_change: true) + end + end + + context 'when the raw diffs are empty' do + before { mr_diff.update_attributes(st_diffs: '') } + + it 'returns an empty DiffCollection' do + expect(mr_diff.diffs).to be_a(Gitlab::Git::DiffCollection) + expect(mr_diff.diffs).to be_empty + end + end + + context 'when the raw diffs exist' do + it 'returns the diffs' do + expect(mr_diff.diffs).to be_a(Gitlab::Git::DiffCollection) + expect(mr_diff.diffs).not_to be_empty + end + + context 'when the :paths option is set' do + let(:diffs) { mr_diff.diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) } + + it 'only returns diffs that match the (old path, new path) given' do + expect(diffs.map(&:new_path)).to contain_exactly('files/ruby/popen.rb') + end + + it 'uses the diffs from the DB' do + expect(mr_diff).to receive(:load_diffs) + + diffs + end + end + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index a4b6ff8f8ad..c8ad7ab3e7f 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -116,6 +116,31 @@ describe MergeRequest, models: true do end end + describe '#diffs' do + let(:merge_request) { build(:merge_request) } + let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } } + + context 'when there are MR diffs' do + it 'delegates to the MR diffs' do + merge_request.merge_request_diff = MergeRequestDiff.new + + expect(merge_request.merge_request_diff).to receive(:diffs).with(options) + + merge_request.diffs(options) + end + end + + context 'when there are no MR diffs' do + it 'delegates to the compare object' do + merge_request.compare = double(:compare) + + expect(merge_request.compare).to receive(:diffs).with(options) + + merge_request.diffs(options) + end + end + end + describe "#mr_and_commit_notes" do let!(:merge_request) { create(:merge_request) } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 1e18c788b50..d661dc0e59a 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -70,7 +70,7 @@ describe Milestone, models: true do end end - describe :expired? do + describe '#expired?' do context "expired" do before do allow(milestone).to receive(:due_date).and_return(Date.today.prev_year) @@ -88,7 +88,7 @@ describe Milestone, models: true do end end - describe :percent_complete do + describe '#percent_complete' do before do allow(milestone).to receive_messages( closed_items_count: 3, @@ -111,11 +111,11 @@ describe Milestone, models: true do it { expect(milestone.is_empty?(user)).to be_falsey } end - describe :can_be_closed? do + describe '#can_be_closed?' do it { expect(milestone.can_be_closed?).to be_truthy } end - describe :total_items_count do + describe '#total_items_count' do before do create :closed_issue, milestone: milestone create :merge_request, milestone: milestone @@ -126,7 +126,7 @@ describe Milestone, models: true do end end - describe :can_be_closed? do + describe '#can_be_closed?' do before do milestone = create :milestone create :closed_issue, milestone: milestone diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 5f68cd2b066..a162da0208e 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -18,11 +18,11 @@ describe Namespace, models: true do it { is_expected.to respond_to(:to_param) } end - describe :to_param do + describe '#to_param' do it { expect(namespace.to_param).to eq(namespace.path) } end - describe :human_name do + describe '#human_name' do it { expect(namespace.human_name).to eq(namespace.owner_name) } end @@ -54,7 +54,7 @@ describe Namespace, models: true do end end - describe :move_dir do + describe '#move_dir' do before do @namespace = create :namespace @project = create :project, namespace: @namespace @@ -98,7 +98,7 @@ describe Namespace, models: true do end end - describe :find_by_path_or_name do + describe '.find_by_path_or_name' do before do @namespace = create(:namespace, name: 'WoW', path: 'woW') end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 6549791f675..7d0697dab42 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -226,6 +226,20 @@ describe Note, models: true do it "returns false" do expect(note.cross_reference_not_visible_for?(private_user)).to be_falsy end + + it "returns false if user visible reference count set" do + note.user_visible_reference_count = 1 + + expect(note).not_to receive(:reference_mentionables) + expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_falsy + end + + it "returns true if ref count is 0" do + note.user_visible_reference_count = 0 + + expect(note).not_to receive(:reference_mentionables) + expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_truthy + end end describe 'clear_blank_line_code!' do diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb index e12258c0874..2142c7c13ef 100644 --- a/spec/models/project_security_spec.rb +++ b/spec/models/project_security_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Project, models: true do - describe :authorization do + describe 'authorization' do before do @p1 = create(:project) diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index 60364df2015..0866e1532dd 100644 --- a/spec/models/project_services/buildkite_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -57,7 +57,7 @@ describe BuildkiteService, models: true do ) end - describe :webhook_url do + 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' @@ -65,7 +65,7 @@ describe BuildkiteService, models: true do end end - describe :commit_status_path do + 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' @@ -73,7 +73,7 @@ describe BuildkiteService, models: true do end end - describe :build_page do + 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' diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 143fd5167a4..53b420d808f 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -130,17 +130,35 @@ describe Project, models: true do end end - it 'should not allow an invalid URI as import_url' do + it 'does not allow an invalid URI as import_url' do project2 = build(:project, import_url: 'invalid://') expect(project2).not_to be_valid end - it 'should allow a valid URI as import_url' do + it 'does allow a valid URI as import_url' do project2 = build(:project, import_url: 'ssh://test@gitlab.com/project.git') expect(project2).to be_valid end + + it 'allows an empty URI' do + project2 = build(:project, import_url: '') + + expect(project2).to be_valid + end + + it 'does not produce import data on an empty URI' do + project2 = build(:project, import_url: '') + + expect(project2.import_data).to be_nil + end + + it 'does not produce import data on an invalid URI' do + project2 = build(:project, import_url: 'test://') + + expect(project2.import_data).to be_nil + end end describe 'default_scope' do @@ -296,7 +314,7 @@ describe Project, models: true do end end - describe :update_merge_requests do + describe '#update_merge_requests' do let(:project) { create(:project) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:key) { create(:key, user_id: project.owner.id) } @@ -345,7 +363,7 @@ describe Project, models: true do end end - describe :to_param do + describe '#to_param' do context 'with namespace' do before do @group = create :group, name: 'gitlab' @@ -356,7 +374,7 @@ describe Project, models: true do end end - describe :repository do + describe '#repository' do let(:project) { create(:project) } it 'returns valid repo' do @@ -364,7 +382,7 @@ describe Project, models: true do end end - describe :default_issues_tracker? do + describe '#default_issues_tracker?' do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } @@ -377,7 +395,7 @@ describe Project, models: true do end end - describe :external_issue_tracker do + describe '#external_issue_tracker' do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } @@ -418,7 +436,7 @@ describe Project, models: true do end end - describe :cache_has_external_issue_tracker do + describe '#cache_has_external_issue_tracker' do let(:project) { create(:project) } it 'stores true if there is any external_issue_tracker' do @@ -440,7 +458,7 @@ describe Project, models: true do end end - describe :open_branches do + describe '#open_branches' do let(:project) { create(:project) } before do @@ -517,7 +535,7 @@ describe Project, models: true do end end - describe :avatar_type do + describe '#avatar_type' do let(:project) { create(:project) } it 'should be true if avatar is image' do @@ -531,7 +549,7 @@ describe Project, models: true do end end - describe :avatar_url do + describe '#avatar_url' do subject { project.avatar_url } let(:project) { create(:project) } @@ -568,7 +586,7 @@ describe Project, models: true do end end - describe :pipeline do + describe '#pipeline' do let(:project) { create :project } let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' } @@ -588,7 +606,7 @@ describe Project, models: true do end end - describe :builds_enabled do + describe '#builds_enabled' do let(:project) { create :project } before { project.builds_enabled = true } @@ -690,7 +708,7 @@ describe Project, models: true do end end - describe :any_runners do + describe '#any_runners' do let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) } let(:specific_runner) { create(:ci_runner) } let(:shared_runner) { create(:ci_runner, :shared) } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 24e49c8def3..b39b958450c 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -16,7 +16,7 @@ describe Repository, models: true do repository.commit(merge_commit_sha) end - describe :branch_names_contains do + describe '#branch_names_contains' do subject { repository.branch_names_contains(sample_commit.id) } it { is_expected.to include('master') } @@ -24,7 +24,7 @@ describe Repository, models: true do it { is_expected.not_to include('fix') } end - describe :tag_names_contains do + describe '#tag_names_contains' do subject { repository.tag_names_contains(sample_commit.id) } it { is_expected.to include('v1.1.0') } @@ -72,13 +72,13 @@ describe Repository, models: true do end end - describe :last_commit_for_path do + describe '#last_commit_for_path' do subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } end - describe :find_commits_by_message do + describe '#find_commits_by_message' do subject { repository.find_commits_by_message('submodule').map{ |k| k.id } } it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } @@ -87,7 +87,7 @@ describe Repository, models: true do it { is_expected.not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') } end - describe :blob_at do + describe '#blob_at' do context 'blank sha' do subject { repository.blob_at(Gitlab::Git::BLANK_SHA, '.gitignore') } @@ -95,7 +95,7 @@ describe Repository, models: true do end end - describe :merged_to_root_ref? do + describe '#merged_to_root_ref?' do context 'merged branch' do subject { repository.merged_to_root_ref?('improve/awesome') } @@ -103,7 +103,7 @@ describe Repository, models: true do end end - describe :can_be_merged? do + describe '#can_be_merged?' do context 'mergeable branches' do subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') } @@ -305,7 +305,7 @@ describe Repository, models: true do end end - describe :add_branch do + describe '#add_branch' do context 'when pre hooks were successful' do it 'should run without errors' do hook = double(trigger: [true, nil]) @@ -349,7 +349,7 @@ describe Repository, models: true do end end - describe :rm_branch do + describe '#rm_branch' do context 'when pre hooks were successful' do it 'should run without errors' do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) @@ -386,7 +386,7 @@ describe Repository, models: true do end end - describe :commit_with_hooks do + describe '#commit_with_hooks' do context 'when pre hooks were successful' do before do expect_any_instance_of(GitHooksService).to receive(:execute). diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 96bbbec9ea1..67b3783d514 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -22,11 +22,11 @@ describe Service, models: true do @testable = @service.can_test? end - describe :can_test do + describe '#can_test?' do it { expect(@testable).to eq(true) } end - describe :test do + describe '#test' do let(:data) { 'test' } it 'test runs execute' do @@ -45,7 +45,7 @@ describe Service, models: true do @testable = @service.can_test? end - describe :can_test do + describe '#can_test?' do it { expect(@testable).to eq(true) } end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3984b30ddf8..fc74488ac0e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -31,6 +31,8 @@ describe User, models: true do it { is_expected.to have_many(:spam_logs).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) } it { is_expected.to have_many(:award_emoji).dependent(:destroy) } + it { is_expected.to have_many(:builds).dependent(:nullify) } + it { is_expected.to have_many(:pipelines).dependent(:nullify) } describe '#group_members' do it 'does not include group memberships for which user is a requester' do @@ -427,7 +429,7 @@ describe User, models: true do end end - describe :not_in_project do + describe '.not_in_project' do before do User.delete_all @user = create :user @@ -598,7 +600,7 @@ describe User, models: true do end end - describe :avatar_type do + describe '#avatar_type' do let(:user) { create(:user) } it "should be true if avatar is image" do @@ -612,7 +614,7 @@ describe User, models: true do end end - describe :requires_ldap_check? do + describe '#requires_ldap_check?' do let(:user) { User.new } it 'is false when LDAP is disabled' do @@ -651,7 +653,7 @@ describe User, models: true do end context 'ldap synchronized user' do - describe :ldap_user? do + describe '#ldap_user?' do it 'is true if provider name starts with ldap' do user = create(:omniauth_user, provider: 'ldapmain') expect(user.ldap_user?).to be_truthy @@ -668,7 +670,7 @@ describe User, models: true do end end - describe :ldap_identity do + describe '#ldap_identity' do it 'returns ldap identity' do user = create :omniauth_user expect(user.ldap_identity.provider).not_to be_empty @@ -825,7 +827,7 @@ describe User, models: true do end end - describe :can_be_removed? do + describe '#can_be_removed?' do subject { create(:user) } context 'no owned groups' do diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 72a6d45f47d..2b74dd4bbb0 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -135,6 +135,22 @@ describe API::API, api: true do expect(response).to have_http_status(401) end + + it "normalizes +1 as thumbsup award" do + post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1' + + expect(issue.award_emoji.last.name).to eq("thumbsup") + end + + context 'when the emoji already has been awarded' do + it 'returns a 404 status code' do + post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' + post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + expect(json_response["message"]).to match("has already been taken") + end + end end end @@ -147,6 +163,22 @@ describe API::API, api: true do expect(response).to have_http_status(201) expect(json_response['user']['username']).to eq(user.username) end + + it "normalizes +1 as thumbsup award" do + post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1' + + expect(note.award_emoji.last.name).to eq("thumbsup") + end + + context 'when the emoji already has been awarded' do + it 'returns a 404 status code' do + post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + + expect(response).to have_http_status(404) + expect(json_response["message"]).to match("has already been taken") + end + end end describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index e567d36afa8..f6f85d6e95e 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -56,13 +56,21 @@ describe API::API, api: true do context "git push with project.wiki" do it 'responds with success' do - project_wiki = create(:project, name: 'my.wiki', path: 'my.wiki') - project_wiki.team << [user, :developer] + push(key, project.wiki) - push(key, project_wiki) + expect(response).to have_http_status(200) + expect(json_response["status"]).to be_truthy + expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo) + end + end + + context "git pull with project.wiki" do + it 'responds with success' do + pull(key, project.wiki) expect(response).to have_http_status(200) expect(json_response["status"]).to be_truthy + expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo) end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 6adccb4ebae..12f2cfa6942 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -503,6 +503,20 @@ describe API::API, api: true do ]) end + context 'with due date' do + it 'creates a new project issue' do + due_date = 2.weeks.from_now.strftime('%Y-%m-%d') + + post api("/projects/#{project.id}/issues", user), + title: 'new issue', due_date: due_date + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['description']).to be_nil + expect(json_response['due_date']).to eq(due_date) + end + end + context 'when an admin or owner makes the request' do it 'accepts the creation date to be set' do creation_time = 2.weeks.ago @@ -683,6 +697,17 @@ describe API::API, api: true do end end + describe 'PUT /projects/:id/issues/:issue_id to update due date' do + it 'creates a new project issue' do + due_date = 2.weeks.from_now.strftime('%Y-%m-%d') + + put api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date + + expect(response).to have_http_status(200) + expect(json_response['due_date']).to eq(due_date) + end + end + describe "DELETE /projects/:id/issues/:issue_id" do it "rejects a non member from deleting an issue" do delete api("/projects/#{project.id}/issues/#{issue.id}", non_member) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 4a1b5600bdf..651b91e9f68 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -138,6 +138,8 @@ describe API::API, api: true do expect(json_response['work_in_progress']).to be_falsy expect(json_response['merge_when_build_succeeds']).to be_falsy expect(json_response['merge_status']).to eq('can_be_merged') + expect(json_response['should_close_merge_request']).to be_falsy + expect(json_response['force_close_merge_request']).to be_falsy end it "should return merge_request" do @@ -147,6 +149,8 @@ describe API::API, api: true do expect(json_response['iid']).to eq(merge_request.iid) expect(json_response['work_in_progress']).to eq(false) expect(json_response['merge_status']).to eq('can_be_merged') + expect(json_response['should_close_merge_request']).to be_falsy + expect(json_response['force_close_merge_request']).to be_falsy end it 'should return merge_request by iid' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 8a52725a893..152cd802839 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -81,6 +81,18 @@ describe API::API, api: true do expect(json_response.first.keys).not_to include('open_issues_count') end + context 'GET /projects?simple=true' do + it 'returns a simplified version of all the projects' do + expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"] + + get api('/projects?simple=true', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first.keys).to match_array expected_keys + end + end + context 'and using search' do it 'should return searched project' do get api('/projects', user), { search: project.name } diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb new file mode 100644 index 00000000000..93d2bc160cc --- /dev/null +++ b/spec/requests/lfs_http_spec.rb @@ -0,0 +1,768 @@ +require 'spec_helper' + +describe Gitlab::Lfs::Router do + let(:user) { create(:user) } + let!(:lfs_object) { create(:lfs_object, :with_file) } + + let(:headers) do + { + 'Authorization' => authorization, + 'X-Sendfile-Type' => sendfile + }.compact + end + let(:authorization) { } + let(:sendfile) { } + + let(:sample_oid) { lfs_object.oid } + let(:sample_size) { lfs_object.size } + + describe 'when lfs is disabled' do + let(:project) { create(:empty_project) } + let(:body) do + { + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }, + { 'oid' => sample_oid, + 'size' => sample_size + } + ], + 'operation' => 'upload' + } + end + + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) + post_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + end + + it 'responds with 501' do + expect(response).to have_http_status(501) + expect(json_response).to include('message' => 'Git LFS is not enabled on this GitLab server, contact your admin.') + end + end + + describe 'deprecated API' do + let(:project) { create(:empty_project) } + + before do + enable_lfs + end + + shared_examples 'a deprecated' do + it 'responds with 501' do + expect(response).to have_http_status(501) + end + + it 'returns deprecated message' do + expect(json_response).to include('message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.') + end + end + + context 'when fetching lfs object using deprecated API' do + let(:authorization) { authorize_user } + + before do + get "#{project.http_url_to_repo}/info/lfs/objects/#{sample_oid}", nil, headers + end + + it_behaves_like 'a deprecated' + end + + context 'when handling lfs request using deprecated API' do + before do + post_json "#{project.http_url_to_repo}/info/lfs/objects", nil, headers + end + + it_behaves_like 'a deprecated' + end + end + + describe 'when fetching lfs object' do + let(:project) { create(:empty_project) } + let(:update_permissions) { } + + before do + enable_lfs + update_permissions + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + end + + context 'and request comes from gitlab-workhorse' do + context 'without user being authorized' do + it 'responds with status 401' do + expect(response).to have_http_status(401) + end + end + + context 'with required headers' do + shared_examples 'responds with a file' do + let(:sendfile) { 'X-Sendfile' } + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'responds with the file location' do + expect(response.headers['Content-Type']).to eq('application/octet-stream') + expect(response.headers['X-Sendfile']).to eq(lfs_object.file.path) + end + end + + context 'with user is authorized' do + let(:authorization) { authorize_user } + + context 'and does not have project access' do + let(:update_permissions) do + project.lfs_objects << lfs_object + end + + it 'responds with status 403' do + expect(response).to have_http_status(403) + end + end + + context 'and does have project access' do + let(:update_permissions) do + project.team << [user, :master] + project.lfs_objects << lfs_object + end + + it_behaves_like 'responds with a file' + end + end + + context 'when CI is authorized' do + let(:authorization) { authorize_ci_project } + + let(:update_permissions) do + project.lfs_objects << lfs_object + end + + it_behaves_like 'responds with a file' + end + end + + context 'without required headers' do + let(:authorization) { authorize_user } + + it 'responds with status 403' do + expect(response).to have_http_status(403) + end + end + end + end + + describe 'when handling lfs batch request' do + let(:update_lfs_permissions) { } + let(:update_user_permissions) { } + + before do + enable_lfs + update_lfs_permissions + update_user_permissions + post_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + end + + describe 'download' do + let(:project) { create(:empty_project) } + let(:body) do + { 'operation' => 'download', + 'objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size + }] + } + end + + shared_examples 'an authorized requests' do + context 'when downloading an lfs object that is assigned to our project' do + let(:update_lfs_permissions) do + project.lfs_objects << lfs_object + end + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'with href to download' do + expect(json_response).to eq('objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size, + 'actions' => { + 'download' => { + 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => { 'Authorization' => authorization } + } + } + }]) + end + end + + context 'when downloading an lfs object that is assigned to other project' do + let(:other_project) { create(:empty_project) } + let(:update_lfs_permissions) do + other_project.lfs_objects << lfs_object + end + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'with href to download' do + expect(json_response).to eq('objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }]) + end + end + + context 'when downloading a lfs object that does not exist' do + let(:body) do + { 'operation' => 'download', + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }] + } + end + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'with an 404 for specific object' do + expect(json_response).to eq('objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }]) + end + end + + context 'when downloading one new and one existing lfs object' do + let(:body) do + { 'operation' => 'download', + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }, + { 'oid' => sample_oid, + 'size' => sample_size + } + ] + } + end + + let(:update_lfs_permissions) do + project.lfs_objects << lfs_object + end + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'responds with upload hypermedia link for the new object' do + expect(json_response).to eq('objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }, + { 'oid' => sample_oid, + 'size' => sample_size, + 'actions' => { + 'download' => { + 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => { 'Authorization' => authorization } + } + } + }]) + end + end + end + + context 'when user is authenticated' do + let(:authorization) { authorize_user } + + let(:update_user_permissions) do + project.team << [user, role] + end + + it_behaves_like 'an authorized requests' do + let(:role) { :reporter } + end + + context 'when user does is not member of the project' do + let(:role) { :guest } + + it 'responds with 403' do + expect(response).to have_http_status(403) + end + end + + context 'when user does not have download access' do + let(:role) { :guest } + + it 'responds with 403' do + expect(response).to have_http_status(403) + end + end + end + + context 'when CI is authorized' do + let(:authorization) { authorize_ci_project } + + it_behaves_like 'an authorized requests' + end + + context 'when user is not authenticated' do + describe 'is accessing public project' do + let(:project) { create(:project, :public) } + + let(:update_lfs_permissions) do + project.lfs_objects << lfs_object + end + + it 'responds with status 200 and href to download' do + expect(response).to have_http_status(200) + end + + it 'responds with status 200 and href to download' do + expect(json_response).to eq('objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size, + 'actions' => { + 'download' => { + 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => {} + } + } + }]) + end + end + + describe 'is accessing non-public project' do + let(:update_lfs_permissions) do + project.lfs_objects << lfs_object + end + + it 'responds with authorization required' do + expect(response).to have_http_status(401) + end + end + end + end + + describe 'upload' do + let(:project) { create(:project, :public) } + let(:body) do + { 'operation' => 'upload', + 'objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size + }] + } + end + + describe 'when request is authenticated' do + describe 'when user has project push access' do + let(:authorization) { authorize_user } + + let(:update_user_permissions) do + project.team << [user, :developer] + end + + context 'when pushing an lfs object that already exists' do + let(:other_project) { create(:empty_project) } + let(:update_lfs_permissions) do + other_project.lfs_objects << lfs_object + end + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'responds with links the object to the project' do + expect(json_response['objects']).to be_kind_of(Array) + expect(json_response['objects'].first['oid']).to eq(sample_oid) + expect(json_response['objects'].first['size']).to eq(sample_size) + expect(lfs_object.projects.pluck(:id)).not_to include(project.id) + expect(lfs_object.projects.pluck(:id)).to include(other_project.id) + expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}") + expect(json_response['objects'].first['actions']['upload']['header']).to eq('Authorization' => authorization) + end + end + + context 'when pushing a lfs object that does not exist' do + let(:body) do + { 'operation' => 'upload', + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }] + } + end + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'responds with upload hypermedia link' do + expect(json_response['objects']).to be_kind_of(Array) + expect(json_response['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") + expect(json_response['objects'].first['size']).to eq(1575078) + expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") + expect(json_response['objects'].first['actions']['upload']['header']).to eq('Authorization' => authorization) + end + end + + context 'when pushing one new and one existing lfs object' do + let(:body) do + { 'operation' => 'upload', + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }, + { 'oid' => sample_oid, + 'size' => sample_size + } + ] + } + end + + let(:update_lfs_permissions) do + project.lfs_objects << lfs_object + end + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'responds with upload hypermedia link for the new object' do + expect(json_response['objects']).to be_kind_of(Array) + + expect(json_response['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") + expect(json_response['objects'].first['size']).to eq(1575078) + expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{project.http_url_to_repo}/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") + expect(json_response['objects'].first['actions']['upload']['header']).to eq("Authorization" => authorization) + + expect(json_response['objects'].last['oid']).to eq(sample_oid) + expect(json_response['objects'].last['size']).to eq(sample_size) + expect(json_response['objects'].last).not_to have_key('actions') + end + end + end + + context 'when user does not have push access' do + let(:authorization) { authorize_user } + + it 'responds with 403' do + expect(response).to have_http_status(403) + end + end + + context 'when CI is authorized' do + let(:authorization) { authorize_ci_project } + + it 'responds with 401' do + expect(response).to have_http_status(401) + end + end + end + + context 'when user is not authenticated' do + context 'when user has push access' do + let(:update_user_permissions) do + project.team << [user, :master] + end + + it 'responds with status 401' do + expect(response).to have_http_status(401) + end + end + + context 'when user does not have push access' do + it 'responds with status 401' do + expect(response).to have_http_status(401) + end + end + end + + context 'when CI is authorized' do + let(:authorization) { authorize_ci_project } + + it 'responds with status 403' do + expect(response).to have_http_status(401) + end + end + end + + describe 'unsupported' do + let(:project) { create(:empty_project) } + let(:body) do + { 'operation' => 'other', + 'objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size + }] + } + end + + it 'responds with status 404' do + expect(response).to have_http_status(404) + end + end + end + + describe 'when pushing a lfs object' do + before do + enable_lfs + end + + shared_examples 'unauthorized' do + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + put_authorize + end + + it 'responds with status 401' do + expect(response).to have_http_status(401) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + put_finalize + end + + it 'responds with status 401' do + expect(response).to have_http_status(401) + end + end + + context 'and request is sent with a malformed headers' do + before do + put_finalize('cat /etc/passwd') + end + + it 'does not recognize it as a valid lfs command' do + expect(response).to have_http_status(403) + end + end + end + + shared_examples 'forbidden' do + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + put_authorize + end + + it 'responds with 403' do + expect(response).to have_http_status(403) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + put_finalize + end + + it 'responds with 403' do + expect(response).to have_http_status(403) + end + end + end + + describe 'to one project' do + let(:project) { create(:empty_project) } + + describe 'when user is authenticated' do + let(:authorization) { authorize_user } + + describe 'when user has push access to the project' do + before do + project.team << [user, :developer] + end + + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + put_authorize + end + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'responds with status 200, location of lfs store and object details' do + expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload") + expect(json_response['LfsOid']).to eq(sample_oid) + expect(json_response['LfsSize']).to eq(sample_size) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + put_finalize + end + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'lfs object is linked to the project' do + expect(lfs_object.projects.pluck(:id)).to include(project.id) + end + end + end + + describe 'and user does not have push access' do + it_behaves_like 'forbidden' + end + end + + context 'when CI is authenticated' do + let(:authorization) { authorize_ci_project } + + it_behaves_like 'unauthorized' + end + + context 'for unauthenticated' do + it_behaves_like 'unauthorized' + end + end + + describe 'to a forked project' do + let(:upstream_project) { create(:project, :public) } + let(:project_owner) { create(:user) } + let(:project) { fork_project(upstream_project, project_owner) } + + describe 'when user is authenticated' do + let(:authorization) { authorize_user } + + describe 'when user has push access to the project' do + before do + project.team << [user, :developer] + end + + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + put_authorize + end + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'with location of lfs store and object details' do + expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload") + expect(json_response['LfsOid']).to eq(sample_oid) + expect(json_response['LfsSize']).to eq(sample_size) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + put_finalize + end + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'lfs object is linked to the source project' do + expect(lfs_object.projects.pluck(:id)).to include(upstream_project.id) + end + end + end + + describe 'and user does not have push access' do + it_behaves_like 'forbidden' + end + end + + context 'when CI is authenticated' do + let(:authorization) { authorize_ci_project } + + it_behaves_like 'unauthorized' + end + + context 'for unauthenticated' do + it_behaves_like 'unauthorized' + end + + describe 'and second project not related to fork or a source project' do + let(:second_project) { create(:empty_project) } + let(:authorization) { authorize_user } + + before do + second_project.team << [user, :master] + upstream_project.lfs_objects << lfs_object + end + + context 'when pushing the same lfs object to the second project' do + before do + put "#{second_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", nil, + headers.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file).compact + end + + it 'responds with status 200' do + expect(response).to have_http_status(200) + end + + it 'links the lfs object to the project' do + expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id) + end + end + end + end + + def put_authorize + put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, headers + end + + def put_finalize(lfs_tmp = lfs_tmp_file) + put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", nil, + headers.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp).compact + end + + def lfs_tmp_file + "#{sample_oid}012345678" + end + end + + def enable_lfs + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + end + + def authorize_ci_project + ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', project.runners_token) + end + + def authorize_user + ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) + end + + def fork_project(project, user, object = nil) + allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) + Projects::ForkService.new(project, user, {}).execute + end + + def post_json(url, body = nil, headers = nil) + post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/json')) + end + + def json_response + @json_response ||= JSON.parse(response.body) + end +end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 8a8e131c57b..2c755919456 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -98,7 +98,7 @@ describe SnippetsController, "routing" do end # help GET /help(.:format) help#index -# help_page GET /help/:category/:file(.:format) help#show {:category=>/.*/, :file=>/[^\/\.]+/} +# help_page GET /help/*path(.:format) help#show # help_shortcuts GET /help/shortcuts(.:format) help#shortcuts # help_ui GET /help/ui(.:format) help#ui describe HelpController, "routing" do @@ -109,23 +109,19 @@ describe HelpController, "routing" do it 'to #show' do path = '/help/markdown/markdown.md' expect(get(path)).to route_to('help#show', - category: 'markdown', - file: 'markdown', + path: 'markdown/markdown', format: 'md') path = '/help/workflow/protected_branches/protected_branches1.png' expect(get(path)).to route_to('help#show', - category: 'workflow/protected_branches', - file: 'protected_branches1', + path: 'workflow/protected_branches/protected_branches1', format: 'png') - end - - it 'to #shortcuts' do - expect(get('/help/shortcuts')).to route_to('help#shortcuts') - end - - it 'to #ui' do - expect(get('/help/ui')).to route_to('help#ui') + path = '/help/shortcuts' + expect(get(path)).to route_to('help#show', + path: 'shortcuts') + path = '/help/ui' + expect(get(path)).to route_to('help#show', + path: 'ui') end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 67777ad48bc..7cc71f706ce 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -87,51 +87,105 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'user authorization' do - let(:project) { create(:project) } let(:current_user) { create(:user) } - context 'allow to use scope-less authentication' do - it_behaves_like 'a valid token' - end + context 'for private project' do + let(:project) { create(:empty_project) } - context 'allow developer to push images' do - before { project.team << [current_user, :developer] } + context 'allow to use scope-less authentication' do + it_behaves_like 'a valid token' + end - let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:push" } + context 'allow developer to push images' do + before { project.team << [current_user, :developer] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push" } + end + + it_behaves_like 'a pushable' end - it_behaves_like 'a pushable' - end + context 'allow reporter to pull images' do + before { project.team << [current_user, :reporter] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end - context 'allow reporter to pull images' do - before { project.team << [current_user, :reporter] } + it_behaves_like 'a pullable' + end - let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull" } + context 'return a least of privileges' do + before { project.team << [current_user, :reporter] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push,pull" } + end + + it_behaves_like 'a pullable' end - it_behaves_like 'a pullable' + context 'disallow guest to pull or push images' do + before { project.team << [current_user, :guest] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull,push" } + end + + it_behaves_like 'an inaccessible' + end end - context 'return a least of privileges' do - before { project.team << [current_user, :reporter] } + context 'for public project' do + let(:project) { create(:empty_project, :public) } - let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:push,pull" } + context 'allow anyone to pull images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end + + it_behaves_like 'a pullable' end - it_behaves_like 'a pullable' + context 'disallow anyone to push images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push" } + end + + it_behaves_like 'an inaccessible' + end end - context 'disallow guest to pull or push images' do - before { project.team << [current_user, :guest] } + context 'for internal project' do + let(:project) { create(:empty_project, :internal) } - let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull,push" } + context 'for internal user' do + context 'allow anyone to pull images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end + + it_behaves_like 'a pullable' + end + + context 'disallow anyone to push images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push" } + end + + it_behaves_like 'an inaccessible' + end end - it_behaves_like 'an inaccessible' + context 'for external user' do + let(:current_user) { create(:user, external: true) } + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull,push" } + end + + it_behaves_like 'an inaccessible' + end end end diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index ae4b7aca820..b72e0bd3dbe 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -9,7 +9,7 @@ describe Ci::CreateTriggerRequestService, services: true do stub_ci_pipeline_to_return_yaml_file end - describe :execute do + describe '#execute' do context 'valid params' do subject { service.execute(project, trigger, 'master') } diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index 476a888e394..3a3e3efe709 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -8,7 +8,7 @@ module Ci let(:commit) { project.ensure_pipeline(commit_sha, 'master') } let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) } - describe :execute do + describe '#execute' do before { build } context 'branch name' do diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index f28f2f1438d..026d0ca6534 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -13,7 +13,7 @@ module Ci specific_runner.assign_to(project) end - describe :execute do + describe '#execute' do context 'runner follow tag list' do it "picks build with the same tag" do pending_build.tag_list = ["linux"] diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index 309213bd44c..d4c5e584421 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -3,13 +3,13 @@ require 'spec_helper' describe CreateCommitBuildsService, services: true do let(:service) { CreateCommitBuildsService.new } let(:project) { FactoryGirl.create(:empty_project) } - let(:user) { nil } + let(:user) { create(:user) } before do stub_ci_pipeline_to_return_yaml_file end - describe :execute do + describe '#execute' do context 'valid params' do let(:pipeline) do service.execute(project, user, @@ -24,6 +24,7 @@ describe CreateCommitBuildsService, services: true do it { expect(pipeline).to be_valid } it { expect(pipeline).to be_persisted } it { expect(pipeline).to eq(project.pipelines.last) } + it { expect(pipeline).to have_attributes(user: user) } it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } end diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 654e441f3cd..8da2a2b3c1b 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -89,6 +89,12 @@ describe CreateDeploymentService, services: true do expect_any_instance_of(described_class).to receive(:execute) subject end + + it 'is set as deployable' do + subject + + expect(Deployment.last.deployable).to eq(deployable) + end end context 'without environment specified' do @@ -105,6 +111,8 @@ describe CreateDeploymentService, services: true do context 'when build succeeds' do it_behaves_like 'does create environment and deployment' do + let(:deployable) { build } + subject { build.success } end end @@ -114,6 +122,14 @@ describe CreateDeploymentService, services: true do subject { build.drop } end end + + context 'when build is retried' do + it_behaves_like 'does create environment and deployment' do + let(:deployable) { Ci::Build.retry(build) } + + subject { deployable.success } + end + end end end end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index f6dc9d4008f..789836f71bb 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -4,7 +4,7 @@ describe EventCreateService, services: true do let(:service) { EventCreateService.new } describe 'Issues' do - describe :open_issue do + describe '#open_issue' do let(:issue) { create(:issue) } it { expect(service.open_issue(issue, issue.author)).to be_truthy } @@ -14,7 +14,7 @@ describe EventCreateService, services: true do end end - describe :close_issue do + describe '#close_issue' do let(:issue) { create(:issue) } it { expect(service.close_issue(issue, issue.author)).to be_truthy } @@ -24,7 +24,7 @@ describe EventCreateService, services: true do end end - describe :reopen_issue do + describe '#reopen_issue' do let(:issue) { create(:issue) } it { expect(service.reopen_issue(issue, issue.author)).to be_truthy } @@ -36,7 +36,7 @@ describe EventCreateService, services: true do end describe 'Merge Requests' do - describe :open_mr do + describe '#open_mr' do let(:merge_request) { create(:merge_request) } it { expect(service.open_mr(merge_request, merge_request.author)).to be_truthy } @@ -46,7 +46,7 @@ describe EventCreateService, services: true do end end - describe :close_mr do + describe '#close_mr' do let(:merge_request) { create(:merge_request) } it { expect(service.close_mr(merge_request, merge_request.author)).to be_truthy } @@ -56,7 +56,7 @@ describe EventCreateService, services: true do end end - describe :merge_mr do + describe '#merge_mr' do let(:merge_request) { create(:merge_request) } it { expect(service.merge_mr(merge_request, merge_request.author)).to be_truthy } @@ -66,7 +66,7 @@ describe EventCreateService, services: true do end end - describe :reopen_mr do + describe '#reopen_mr' do let(:merge_request) { create(:merge_request) } it { expect(service.reopen_mr(merge_request, merge_request.author)).to be_truthy } @@ -80,7 +80,7 @@ describe EventCreateService, services: true do describe 'Milestone' do let(:user) { create :user } - describe :open_milestone do + describe '#open_milestone' do let(:milestone) { create(:milestone) } it { expect(service.open_milestone(milestone, user)).to be_truthy } @@ -90,7 +90,7 @@ describe EventCreateService, services: true do end end - describe :close_mr do + describe '#close_mr' do let(:milestone) { create(:milestone) } it { expect(service.close_milestone(milestone, user)).to be_truthy } @@ -100,7 +100,7 @@ describe EventCreateService, services: true do end end - describe :destroy_mr do + describe '#destroy_mr' do let(:milestone) { create(:milestone) } it { expect(service.destroy_milestone(milestone, user)).to be_truthy } diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 62b25709a5d..67a919ba8ee 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -12,7 +12,7 @@ describe Issues::CloseService, services: true do project.team << [user2, :developer] end - describe :execute do + describe '#execute' do context "valid params" do before do perform_enqueued_jobs do diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 8443a00e70c..c1db4f3284b 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -12,7 +12,7 @@ describe MergeRequests::CloseService, services: true do project.team << [user2, :developer] end - describe :execute do + describe '#execute' do context 'valid params' do let(:service) { MergeRequests::CloseService.new(project, user, {}) } diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index e433f49872d..d0b55d2d509 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -5,7 +5,7 @@ describe MergeRequests::CreateService, services: true do let(:user) { create(:user) } let(:assignee) { create(:user) } - describe :execute do + describe '#execute' do context 'valid params' do let(:opts) do { diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 2f72cd60071..f5bf3c1e367 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -11,7 +11,7 @@ describe MergeRequests::MergeService, services: true do project.team << [user2, :developer] end - describe :execute do + describe '#execute' do context 'valid params' do let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 7d5cb876063..06f56d85aa8 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -5,7 +5,7 @@ describe MergeRequests::RefreshService, services: true do let(:user) { create(:user) } let(:service) { MergeRequests::RefreshService } - describe :execute do + describe '#execute' do before do @user = create(:user) group = create(:group) diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index ac0221998f5..88c9c640514 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -11,7 +11,7 @@ describe MergeRequests::ReopenService, services: true do project.team << [user2, :developer] end - describe :execute do + describe '#execute' do context 'valid params' do let(:service) { MergeRequests::ReopenService.new(project, user, {}) } diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb index 1cd6eb2ab38..5d400299be0 100644 --- a/spec/services/milestones/close_service_spec.rb +++ b/spec/services/milestones/close_service_spec.rb @@ -9,7 +9,7 @@ describe Milestones::CloseService, services: true do project.team << [user, :master] end - describe :execute do + describe '#execute' do before do Milestones::CloseService.new(project, user, {}).execute(milestone) end diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb index c793026e300..6d29edb449a 100644 --- a/spec/services/milestones/create_service_spec.rb +++ b/spec/services/milestones/create_service_spec.rb @@ -4,7 +4,7 @@ describe Milestones::CreateService, services: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } - describe :execute do + describe '#execute' do context "valid params" do before do project.team << [user, :master] diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 35f576874b8..32753e84b31 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -5,7 +5,7 @@ describe Notes::CreateService, services: true do let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } - describe :execute do + describe '#execute' do context "valid params" do before do project.team << [user, :master] diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb index d4c50f824c1..e33a611929b 100644 --- a/spec/services/notes/post_process_service_spec.rb +++ b/spec/services/notes/post_process_service_spec.rb @@ -5,7 +5,7 @@ describe Notes::PostProcessService, services: true do let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } - describe :execute do + describe '#execute' do before do project.team << [user, :master] note_opts = { diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 54719cbb8d8..9fc93f325f7 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -50,7 +50,7 @@ describe NotificationService, services: true do update_custom_notification(:new_note, @u_custom_global) end - describe :new_note do + describe '#new_note' do it do add_users_with_subscription(note.project, issue) @@ -293,6 +293,30 @@ describe NotificationService, services: true do end end end + + context "merge request diff note" do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request, source_project: project, assignee: user) } + let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) } + + before do + build_team(note.project) + project.team << [merge_request.author, :master] + project.team << [merge_request.assignee, :master] + end + + describe '#new_note' do + it "records sent notifications" do + # Ensure create SentNotification by noteable = merge_request 6 times, not noteable = note + expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(4).times.and_call_original + + notification.new_note(note) + + expect(SentNotification.last.position).to eq(note.position) + end + end + end end describe 'Issues' do diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index bd4dc6a0f79..ad0d58672b3 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -12,18 +12,28 @@ describe Projects::HousekeepingService do it 'enqueues a sidekiq job' do expect(subject).to receive(:try_obtain_lease).and_return(true) - expect(GitlabShellOneShotWorker).to receive(:perform_async).with(:gc, project.repository_storage_path, project.path_with_namespace) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id) subject.execute - expect(project.pushes_since_gc).to eq(0) + expect(project.reload.pushes_since_gc).to eq(0) end - it 'does not enqueue a job when no lease can be obtained' do - expect(subject).to receive(:try_obtain_lease).and_return(false) - expect(GitlabShellOneShotWorker).not_to receive(:perform_async) + context 'when no lease can be obtained' do + before(:each) do + expect(subject).to receive(:try_obtain_lease).and_return(false) + end - expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken) - expect(project.pushes_since_gc).to eq(0) + it 'does not enqueue a job' do + expect(GitGarbageCollectWorker).not_to receive(:perform_async) + + expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken) + end + + it 'does not reset pushes_since_gc' do + expect do + expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken) + end.not_to change { project.pushes_since_gc }.from(3) + end end end @@ -39,10 +49,24 @@ describe Projects::HousekeepingService do end describe 'increment!' do + let(:lease_key) { "project_housekeeping:increment!:#{project.id}" } + it 'increments the pushes_since_gc counter' do - expect(project.pushes_since_gc).to eq(0) - subject.increment! - expect(project.pushes_since_gc).to eq(1) + lease = double(:lease, try_obtain: true) + expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease) + + expect do + subject.increment! + end.to change { project.pushes_since_gc }.from(0).to(1) + end + + it 'does not increment when no lease can be obtained' do + lease = double(:lease, try_obtain: false) + expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease) + + expect do + subject.increment! + end.not_to change { project.pushes_since_gc } end end end diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb index f034f251ba4..4f47e89b4b5 100644 --- a/spec/services/test_hook_service_spec.rb +++ b/spec/services/test_hook_service_spec.rb @@ -5,7 +5,7 @@ describe TestHookService, services: true do let(:project) { create :project } let(:hook) { create :project_hook, project: project } - describe :execute do + describe '#execute' do it "should execute successfully" do stub_request(:post, hook.url).to_return(status: 200) expect(TestHookService.new.execute(hook, user)).to be_truthy diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index b4522536724..34d8ea9090e 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -35,8 +35,11 @@ describe TodoService, services: true do should_not_create_any_todo { service.new_issue(unassigned_issue, author) } end - it 'does not create a todo if assignee is the current user' do - should_not_create_any_todo { service.new_issue(unassigned_issue, john_doe) } + it 'creates a todo if assignee is the current user' do + unassigned_issue.update_attribute(:assignee, john_doe) + service.new_issue(unassigned_issue, john_doe) + + should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED) end it 'creates a todo for each valid mentioned user' do @@ -44,7 +47,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: issue, action: Todo::MENTIONED) should_create_todo(user: guest, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) + should_create_todo(user: author, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) end @@ -57,7 +60,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) - should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end context 'when a private group is mentioned' do @@ -87,7 +90,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: issue, action: Todo::MENTIONED) should_create_todo(user: guest, target: issue, action: Todo::MENTIONED) should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) + should_create_todo(user: author, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) end @@ -105,7 +108,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) - should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end context 'issues with a task list' do @@ -156,10 +159,11 @@ describe TodoService, services: true do should_not_create_any_todo { service.reassigned_issue(issue, author) } end - it 'does not create a todo if new assignee is the current user' do + it 'creates a todo if new assignee is the current user' do unassigned_issue.update_attribute(:assignee, john_doe) + service.reassigned_issue(unassigned_issue, john_doe) - should_not_create_any_todo { service.reassigned_issue(unassigned_issue, john_doe) } + should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED) end end @@ -250,7 +254,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) - should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) end @@ -262,7 +266,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) - should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) + should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) end it 'creates a todo for each valid mentioned user when leaving a note on commit' do @@ -270,7 +274,7 @@ describe TodoService, services: true do should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - should_not_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) end @@ -312,7 +316,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) + should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) end @@ -325,7 +329,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) + should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) end @@ -382,10 +386,11 @@ describe TodoService, services: true do should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, author) } end - it 'does not create a todo if new assignee is the current user' do + it 'creates a todo if new assignee is the current user' do mr_assigned.update_attribute(:assignee, john_doe) + service.reassigned_merge_request(mr_assigned, john_doe) - should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, john_doe) } + should_create_todo(user: john_doe, target: mr_assigned, author: john_doe, action: Todo::ASSIGNED) end end @@ -435,6 +440,24 @@ describe TodoService, services: true do should_create_todo(user: author, target: mr_unassigned, action: Todo::MARKED) end end + + describe '#new_note' do + let(:mention) { john_doe.to_reference } + let(:diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") } + let(:legacy_diff_note_on_merge_request) { create(:legacy_diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") } + + it 'creates a todo for mentioned user on new diff note' do + service.new_note(diff_note_on_merge_request, author) + + should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: diff_note_on_merge_request) + end + + it 'creates a todo for mentioned user on legacy diff note' do + service.new_note(legacy_diff_note_on_merge_request, author) + + should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: legacy_diff_note_on_merge_request) + end + end end it 'updates cached counts when a todo is created' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 62097de2768..70d516e05e6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -33,7 +33,6 @@ RSpec.configure do |config| config.include LoginHelpers, type: :feature config.include StubConfiguration config.include EmailHelpers - config.include RelativeUrl, type: feature config.include TestEnv config.include ActiveJob::TestHelper config.include StubGitlabCalls diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb index 9b5c3065eed..b57a3493aff 100644 --- a/spec/support/capybara_helpers.rb +++ b/spec/support/capybara_helpers.rb @@ -27,6 +27,14 @@ module CapybaraHelpers end end end + + # Refresh the page. Calling `visit current_url` doesn't seem to work consistently. + # + def refresh + url = current_url + visit 'about:blank' + visit url + end end RSpec.configure do |config| diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb index 553fe9f1fbc..f550e9a0160 100644 --- a/spec/support/fake_u2f_device.rb +++ b/spec/support/fake_u2f_device.rb @@ -18,8 +18,8 @@ class FakeU2fDevice def respond_to_u2f_authentication app_id = @page.evaluate_script('gon.u2f.app_id') - challenges = @page.evaluate_script('gon.u2f.challenges') - json_response = u2f_device(app_id).sign_response(challenges[0]) + challenge = @page.evaluate_script('gon.u2f.challenge') + json_response = u2f_device(app_id).sign_response(challenge) @page.execute_script(" u2f.sign = function(appId, challenges, signRequests, callback) { diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index ffdf2bb0a8a..e5f76afbfc0 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -37,6 +37,40 @@ module LoginHelpers Thread.current[:current_user] = user end + def login_via(provider, user, uid) + mock_auth_hash(provider, uid, user.email) + visit new_user_session_path + click_link provider + end + + def mock_auth_hash(provider, uid, email) + # The mock_auth configuration allows you to set per-provider (or default) + # authentication hashes to return during integration testing. + OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({ + provider: provider, + uid: uid, + info: { + name: 'mockuser', + email: email, + image: 'mock_user_thumbnail_url' + }, + credentials: { + token: 'mock_token', + secret: 'mock_secret' + }, + extra: { + raw_info: { + info: { + name: 'mockuser', + email: email, + image: 'mock_user_thumbnail_url' + } + } + } + }) + Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:saml] + end + # Requires Javascript driver. def logout find(".header-user-dropdown-toggle").click diff --git a/spec/support/omni_auth.rb b/spec/support/omni_auth.rb new file mode 100644 index 00000000000..0b1af4052ff --- /dev/null +++ b/spec/support/omni_auth.rb @@ -0,0 +1 @@ +OmniAuth.config.test_mode = true diff --git a/spec/support/relative_url.rb b/spec/support/relative_url.rb deleted file mode 100644 index 72e3ccce75b..00000000000 --- a/spec/support/relative_url.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Fix route helpers in tests (e.g. root_path, ...) -module RelativeUrl - extend ActiveSupport::Concern - - included do - default_url_options[:script_name] = Rails.application.config.relative_url_root - end -end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 6b99b0f24cb..bb6c84262f6 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -5,19 +5,20 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { - 'empty-branch' => '7efb185', - 'flatten-dir' => 'e56497b', - 'feature' => '0b4bc9a', - 'feature_conflict' => 'bb5206f', - 'fix' => '48f0be4', - 'improve/awesome' => '5937ac0', - 'markdown' => '0ed8c6c', - 'lfs' => 'be93687', - 'master' => '5937ac0', - "'test'" => 'e56497b', - 'orphaned-branch' => '45127a9', - 'binary-encoding' => '7b1cf43', - 'gitattributes' => '5a62481', + 'empty-branch' => '7efb185', + 'flatten-dir' => 'e56497b', + 'feature' => '0b4bc9a', + 'feature_conflict' => 'bb5206f', + 'fix' => '48f0be4', + 'improve/awesome' => '5937ac0', + 'markdown' => '0ed8c6c', + 'lfs' => 'be93687', + 'master' => '5937ac0', + "'test'" => 'e56497b', + 'orphaned-branch' => '45127a9', + 'binary-encoding' => '7b1cf43', + 'gitattributes' => '5a62481', + 'expand-collapse-diffs' => '4842455' } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb new file mode 100644 index 00000000000..c9f5aae0815 --- /dev/null +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe GitGarbageCollectWorker do + let(:project) { create(:project) } + let(:shell) { Gitlab::Shell.new } + + subject { GitGarbageCollectWorker.new } + + before do + allow(subject).to receive(:gitlab_shell).and_return(shell) + end + + describe "#perform" do + it "runs `git gc`" do + expect(shell).to receive(:gc).with( + project.repository_storage_path, + project.path_with_namespace). + and_return(true) + expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).to receive(:branch_count).and_call_original + expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original + + subject.perform(project.id) + end + end +end |